stavroskasidis / BlazorContextMenu

A context menu component for Blazor !
MIT License
530 stars 58 forks source link

Checkbox functionality on menu #75

Closed tlemmons closed 4 years ago

tlemmons commented 4 years ago

Is there a way to have a menu item show a check or check box. I would like to be able to turn the check on and off without actually closing the menu. This would be used for adding/removing a song on several playlists.

stavroskasidis commented 4 years ago

Sure, here is an example. I am also working on making improvements so that this is easier, but for now this will do for your case:

<ContextMenu Id="tableContextMenu">
    <Item OnClick="@PlaylistClick" OnAppearing="OnAppearing"> <i class="@AddRemoveIcon"></i> @AddRemoveText</Item>
</ContextMenu>

<table class="table">
    <thead>
        <tr>
            <th>Title</th>
            <th>Other Stuff</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var song in songs)
        {
            <ContextMenuTrigger WrapperTag="tr" MenuId="tableContextMenu" Data="song">
                <td>@song.Title</td>
                <td>@song.SomeOtherStuff</td>
            </ContextMenuTrigger>
        }
     </tbody>
</table>

@code{
    private string AddRemoveIcon;
    private string AddRemoveText;

    void PlaylistClick(ItemClickEventArgs e)
    {
        e.IsCanceled = true;
        var song= e.Data as Song;
        song.Added = !song.Added;
        SetMenuItemText(song);
    }

    void OnAppearing(ItemAppearingEventArgs e)
    {
        var song= e.Data as Song;
        SetMenuItemText(song);
    }

    void SetMenuItemText(Song song)
    {
        if (song.Added)
        {
            AddRemoveIcon = "far fa-square";
            AddRemoveText = "Remove from playlist";
        }
        else
        {
            AddRemoveIcon = "far fa-check-square";
            AddRemoveText = "Add to playlist";
        }
    }
}

Let me know if this covers your case

stavroskasidis commented 4 years ago

Version 1.6 simplifies the above example significantly

<ContextMenu Id="tableContextMenu">
    @{
        var song = context.Data as Song;
        var iconClass = song.Added ? "far fa-square" : "far fa-check-square";
        var text = song.Added ? "Remove from playlist" : "Add to playlist";
        <Item OnClick="@PlaylistClick"> <i class="@iconClass"></i> @text</Item>
    }
</ContextMenu>

<table class="table">
    <thead>
        <tr>
            <th>Title</th>
            <th>Other Stuff</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var song in songs)
        {
            <ContextMenuTrigger WrapperTag="tr" MenuId="tableContextMenu" Data="song">
                <td>@song.Title</td>
                <td>@song.SomeOtherStuff</td>
            </ContextMenuTrigger>
        }
     </tbody>
</table>

@code{
    void PlaylistClick(ItemClickEventArgs e)
    {
        e.IsCanceled = true;
        var song= e.Data as Song;
        song.Added = !song.Added;
    }
}
stavroskasidis commented 4 years ago

Closing assuming the provided solution works. Reopen if it doesnt

tlemmons commented 4 years ago

Sorry I have just gotten back from holiday travel. And I cannot figure out how to reopen so hoping another comment is enough.

I tried your example on 1.6 and am having problems with creating submenu items. My code is included below. The item for the submenu is created in the main menu but the submenu is not filled out. The code that should create multiple submenu items never get run. I am pretty new to these components. I have verified that the collection list gets created and populated during the onInitialized but the foreach loop in the submenu layout does not ever get called. Any help appreciated.

    <ContextMenu` Id="myMenu">
    <Item OnClick="@OnClick">Edit</Item>
    <Item OnClick="@OnClick">Delete</Item>
    <Seperator />
    <Item>
        Collections
        <SubMenu>
            @{
                foreach (collectionData c in collections)
                {
                    var name = c.Name;
                    var iconClass = c.selected ? "far fa-square" : "far fa-check-square";
                    <Item OnClick="@PlaylistClick"> <i class="@iconClass"></i> @name</Item>
                }
            }
        </SubMenu>
    </Item>
</ContextMenu>

<ContextMenuTrigger MenuId="myMenu">
    <img src="https://www.nimbusframe.net/getImage.ashx?Id=@ID&height=150&type=1&crop=0" />
</ContextMenuTrigger>

@code {
    [CascadingParameter] protected Users currentUser { get; set; }

    [Parameter]
    public string ID { get; set; }

    public struct collectionData
    {
        public String Name;
        public bool selected;
    }
    List<collectionData> collections = new List<collectionData>();

    protected override void OnInitialized()
    {
        nimbusContext nc = new nimbusContext();
        var cols = nc.Collections.Where(collection => collection.UId == currentUser.Id);
        collections.Clear();
        foreach (Collections c in cols)
        {
            collectionData tmp = new collectionData();
            tmp.Name = c.Name;
            tmp.selected = c.isImageIncluded(Int32.Parse(ID));
            collections.Add(tmp);
        }
    }

    void OnClick(ItemClickEventArgs e)
    {
        Console.WriteLine($"Item Clicked => Menu: {e.ContextMenuId}, MenuTarget: {e.ContextMenuTargetId}, IsCanceled: {e.IsCanceled}, MenuItem: {e.MenuItemElement}, MouseEvent: {e.MouseEvent}");
    }
    void PlaylistClick(ItemClickEventArgs e)
    {
        Console.WriteLine($"Item Clicked => Menu: {e.ContextMenuId}, MenuTarget: {e.ContextMenuTargetId}, IsCanceled: {e.IsCanceled}, MenuItem: {e.MenuItemElement}, MouseEvent: {e.MouseEvent}");
    }

}
stavroskasidis commented 4 years ago

I will investigate this asap. The code is seemingly correct. I will get back to you on this

stavroskasidis commented 4 years ago

I managed to produce a working sample based on your code. I had to change collectionData from a struct to a class, so that it get passed as a reference and also I had to fake the data obviously.

<ContextMenu Id="myMenu">
    <Item OnClick="@OnClick">Edit</Item>
    <Item OnClick="@OnClick">Delete</Item>
    <Seperator />
    <Item>
        Collections
        <SubMenu>
            @{
                foreach (var c in collections)
                {
                    var iconClass = c.selected ? "far fa-check-square" : "far fa-square";
                    <Item OnClick="@((e)=> PlaylistClick(e,c))"> <i class="@iconClass"></i> @c.Name</Item>
                }
            }
        </SubMenu>
    </Item>
</ContextMenu>
<ContextMenuTrigger MenuId="myMenu">
    Open menu
</ContextMenuTrigger>

@code {
    public class collectionData
    {
        public String Name { get; set; }
        public bool selected { get; set; }
    }

    List<collectionData> collections = new List<collectionData>();

    protected override void OnInitialized()
    {
        for(int i=0; i < 5; i++)
        {
            var tmp = new collectionData();
            tmp.Name = $"Collection {i+ 1}";
            tmp.selected = false;
            collections.Add(tmp);
        }
    }

    void OnClick(ItemClickEventArgs e)
    {
        Console.WriteLine($"Item Clicked => Menu: {e.ContextMenuId}, MenuTarget: {e.ContextMenuTargetId}, IsCanceled: {e.IsCanceled}, MenuItem: {e.MenuItemElement}, MouseEvent: {e.MouseEvent}");
    }

    void PlaylistClick(ItemClickEventArgs e, collectionData collectionData)
    {
        e.IsCanceled = true;
        collectionData.selected = !collectionData.selected;
    }
}

This works, maybe the problem was the struct or there is some other error with your code ?

tlemmons commented 4 years ago

I took your code as is and replaced mine, It still does not create the submenu items. Could we have something else different in our setup? I am on the latest nuget package dated Friday, December 27, 2019.

My code above this does create many of these components, could multiple on a page be a problem?

image

tlemmons commented 4 years ago

Tested and if I only do one item with a context menu, it works. Multiple menus seems to cause all submenu items to not be shown. I need multiple different submenus since the checked items will be unique. Any ideas?

stavroskasidis commented 4 years ago

I cannot reproduce what you are describing. However I think that there is some misunderstanding on how the component works. Here is a list of things/facts to check:

  1. You can have multiple <ContextMenu> components in a page, just make sure that they have different ids.
  2. A ContextMenu does not have to be on the same page/component, it can be anywhere inside your App, because it is being looked up by id, so as long as it is rendered somewhere, it will show up.
  3. The concept is, you create a <ContextMenu> for the same content with one or more ContextMenuTriggers.
  4. If you want the items of the context menu to be dynamic, based on the clicked item, you can use the ContextMenuTrigger's Data parameter and the @context variable in the ContextMenu, and using the @context.Data property your code can take decisions depending on the passed data. Here is the example reworked, if my wall of text does not make sense:
<ContextMenu Id="myMenu">
    @if (((MenuType)context.Data) == MenuType.Type1)
    {
        <Item OnClick="@OnClick">Menu type 1</Item>
        <Seperator />
        <Item>
            Collections
            <SubMenu>
                @{
                    foreach (var c in collections)
                    {
                        var iconClass = c.selected ? "far fa-check-square" : "far fa-square";
                        <Item OnClick="@((e) => PlaylistClick(e, c))"> <i class="@iconClass"></i> @c.Name</Item>
                    }
                }
            </SubMenu>
        </Item>
    }
    else
    {
        <Item OnClick="@OnClick">Menu type 2</Item>
        <Seperator />
    }
</ContextMenu>

<ContextMenuTrigger MenuId="myMenu" Data="MenuType.Type1">
    Open menu type 1
</ContextMenuTrigger>

<ContextMenuTrigger MenuId="myMenu" Data="MenuType.Type2">
    Open menu type 2
</ContextMenuTrigger>

@code {

    enum MenuType
    {
        Type1,
        Type2
    }

    public class collectionData
    {
        public String Name { get; set; }
        public bool selected { get; set; }
    }

    List<collectionData> collections = new List<collectionData>();

    protected override void OnInitialized()
    {
        for (int i = 0; i < 5; i++)
        {
            var tmp = new collectionData();
            tmp.Name = $"Collection {i + 1}";
            tmp.selected = false;
            collections.Add(tmp);
        }
    }

    void OnClick(ItemClickEventArgs e)
    {
        Console.WriteLine($"Item Clicked => Menu: {e.ContextMenuId}, MenuTarget: {e.ContextMenuTargetId}, IsCanceled: {e.IsCanceled}, MenuItem: {e.MenuItemElement}, MouseEvent: {e.MouseEvent}");
    }

    void PlaylistClick(ItemClickEventArgs e, collectionData collectionData)
    {
        e.IsCanceled = true;
        collectionData.selected = !collectionData.selected;
    }
}

Am I getting closer to what you want to achieve ?

tlemmons commented 4 years ago

I think the problem is that I have the same menu Id for each one on the overall page. I need to figure out a way to dynamically create the Ids before the menu is generated. I am trying to figure that out now.

stavroskasidis commented 4 years ago

Why not create a single context menu, outside of your loop, and pass data to it from the trigger, so that the content of the menu is dynamic? This is what I recommend doing.

tlemmons commented 4 years ago

That makes sense. Are you talking about doing the same thing you did here with dynamic items, https://blazor-context-menu-demo.azurewebsites.net/dynamicitems by using onAppearing or is there a way to do it from the actual trigger event so it loops/sets all of them at once.

stavroskasidis commented 4 years ago

Yes, you can use the OnAppearing event or use the @context.Data showcased in the above examples. It is the same thing, since the ContextMenu does not render its contents until shown.

tlemmons commented 4 years ago

Thanks for all the help. I think I have it working well.