Blazor Dropdown with Search field - Handle Onfocusout - c#

I have a dropdown with a search field to filter the list. Here is the basic structure:
<div class="dropdown">
<button class="dropdown-button dropdown-toggle" #onclick="e => this.show = !this.show"></button>
<div class="dropdown-menu #(show ? "show" : "")">
<input class="form-control form-control-sm" placeholder="type filter..."/>
<div class="scrollable-menu">
<table>
...
</table>
</div>
</div>
</div>
How do I hide the dropdown when the user clicks somewhere else?
If I use the onblur event of the button the dropdown gets hidden when the user clicks inside the filter input --> doesnt work.
The dropdown-menu is outside the dropdown div so I can't use that.
It would be ideal if I could group the button and the dropdown list together somehow so that the focusout event only gets triggered when the user clicks outside this "group" of elements.
EDIT
I updated the code snipped to show where how I toggle the dropdown.
The show variable is also inverted when the user selects an element in the list.

The simplest way is to use CSS - hide the scrollable-menu by default, then display it when anything in the dropdown has focus.
.scrollable-menu {
display: none;
}
.dropdown:focus-within .scrollable-menu {
display: block;
}
Edit: Add more complicated Blazor event based version
This problem (which I had not understood fully before) is usually solved in javascript detecting whether the target element of a focus change is within the container, but that then means interop calls to set/update your show field.
A purely Blazor solution could be handled by delaying the hide and cancelling if focus remains inside.
<div class="dropdown">
<button class="dropdown-button dropdown-toggle" #onclick=HandleClick #onfocus=HandleFocus #onblur=HandleBlur ></button>
<div class="dropdown-menu #(show ? "show" : "")" #onfocusin=HandleFocus #onfocusout=HandleBlur tabindex="-1">
<input class="form-control form-control-sm" placeholder="type filter..."/>
<div class="scrollable-menu">
<table>
...
</table>
</div>
</div>
</div>
#code{
bool show;
CancellationTokenSource tokenSource;
void HandleClick() => show = !show;
async Task HandleBlur(FocusEventArgs a)
{
tokenSource = new CancellationTokenSource();
await Task.Factory.StartNew(async ()=> {
await Task.Delay(100);
show = false;
await InvokeAsync(StateHasChanged);
},tokenSource.Token);
}
void HandleFocus(FocusEventArgs a)
{
if (tokenSource is CancellationTokenSource)
tokenSource.Cancel();
}
}
Try it out here: https://blazorrepl.com/repl/wFESlpaa33iocZJR52

use InvokeVoidAsync. It will not work if you make it async.
#onclick="#(e => {
JsRuntime.InvokeVoidAsync("eval",$"bootstrap.Dropdown(yourID).toggle()");
})"

Related

How to generate a Blazor component dynamically on a click of a button?

I am working with Blazor server. I have a ButtonComponent which displays a button and the user can pass in a label as a parameter via a modal window and then click Ok.
Once the user clicks Ok, the label typed in gets displayed on the button.
How can I display multiple components vertically(ButtonComponent)
when the user clicks the Show Button one at a time?
#page "/tester"
<button type="button" #onclick="() => showSignUpComponent = true">
Show Button
</button>
#if (showSignUpComponent)
{
<ButtonComponent label="Search" /> <hr/>
}
#code {
bool showSignUpComponent;
}
Rather than thinking of collections of controls, think of Lists of objects that need to be expressed in markup. So, if you have a collection of Label strings that need buttons, you can do something like:
#page "/btn"
<input #bind="newLabel" />
<button type="button" #onclick="() => Labels.Add(newLabel) ">
Spawn
</button>
#foreach (var label in Labels)
{
#*<ButtonComponent label="#label" />*#
<div>This would be a button with label "#label".</div>
}
#code {
string newLabel { get; set; } = "";
List<string> Labels = new List<string>();
}
You can list any class you want, and swap controls in/out as you need.

Selenium clicking button hidden by span

I am trying to automate an environment selection screen where there are multiple selectable buttons individually hidden by a span, these display as tiles.
I have managed to navigate to a given tile and pull up the button but I am unable to click it.
Here is the code I have
public static void NavigateToEnvironment(IWebDriver driver, string environment)
{
IWait<IWebDriver> wait = new WebDriverWait(driver, TimeSpan.FromSeconds(5.00));
wait.Until(ExpectedConditions.ElementIsVisible(By.XPath($"//span[text()='{environment}']")));
var tile = driver.FindElement(By.XPath($"//span[text()='{environment}']"));
Actions action = new Actions(driver);
action.MoveToElement(tile).Perform();
wait.Until(ExpectedConditions.ElementIsVisible(By.XPath($"//*[#span=(text()='{environment}')][#btn=(starts-with(text(), 'Start'))]")));
driver.FindElement(By.XPath($"//*[starts-with(text(), 'Start')]")).Click();
}
The first part successfully moves to the correct tile and opens the span so on screen the button is there.
The wait.until condition is fine too so Selenium can see the element so its the final click command I have an issue with.
It seems only to look for the button hidden within tile one but I am trying tile three. All the buttons have the same HTML tags.
In the current code state I get element not visible.
I have tried to use the xpath as in the wait condition but that returns that the parameters are not elements so again fails.
I am kind of at a loss. Any ideas?
UPDATE:
Some HTML of one of the buttons. This basically repeats with a different application name
<li class="trans tile">
<div class="tileWrap noselect" aria-haspopup="true">
<div class="divNavIcon">
<span class="spnNavIcon primarycolorfont enable" data-bind="css: Code"></span>
</div>
<div class="tilePopup primarycolor">
<span data-bind="text: ApplicationNameAlias ? ApplicationNameAlias : ApplicationName">Enable QA</span>
<span data-bind="text: Description" class="tileSubText">Enable CI Environment</span>
<div class="tilePopupToggle">
<button type="button" data-bind="click: $parent.startApp, css: { disabled: IsRevoked }" class="btn">Start <i class="fa fa-fw fa-desktop"></i></button>
<button type="button" style="display:none;" data-bind="click: $parent.startAppNew, css: { disabled: IsRevoked }" class="btn">Start New <i class="fa fa-fw fa-external-link"></i></button>
<button type="button" style="display:none;" data-bind="attr: { "data-target": "#appPreview_" + ApplicationID }" class="btn" data-toggle="modal" data-target="#appPreview_3043">Preview <i class="fa fa-fw fa-play"></i></button>
</div>
</div>
</div>
Screenshot to help understanding - Each tile acts in the same way with a hidden start button. My code works fine for this first tile but if I want the second or third tiles it cannot find the start button
As per the HTML you have shared to click on the button with text as Start you can use the following code block :
wait.Until(ExpectedConditions.ElementToBeClickable(By.XPath("//div[#class='tilePopup primarycolor']//div[#class='tilePopupToggle']/button[#class='btn' and normalize-space()='Start']/i[#class='fa fa-fw fa-desktop']"))).Click();
Update
Can you try removing the <button> tag as :
wait.Until(ExpectedConditions.ElementToBeClickable(By.XPath("//div[#class='tilePopup primarycolor']//div[#class='tilePopupToggle']//i[#class='fa fa-fw fa-desktop']"))).Click();
Note : As per aurelia/binding/issues/163 disable.bind disables button but inner content is still clickable and we are targeting i[#class='fa fa-fw fa-desktop']
I have managed a pretty elegant work around to this issue. The buttons are contained in li items so i'm just finding the relevant one of those.
public void NavigateToEnvironment(IWebDriver driver, string environment)
{
var tile = driver.FindElement(By.XPath($"//span[text()='{environment}']"),5);
Actions action = new Actions(driver);
action.MoveToElement(tile).Perform();
var tile2 = driver
.FindElement(By.XPath("//*[#id='content']/div/div/div/div/ul"))
.FindElements(By.TagName("li"))
.Where(x => !string.IsNullOrWhiteSpace(x.Text))
.ToList();
var singleTile = tile2.Single(x => x.Text.Contains(environment));
driver.FindElement(By.XPath($"//*[#id='content']/div/div/div/div/ul/li[{tile2.IndexOf(singleTile) + 1}]/div[1]/div[2]/div/button[1]")).Click();
}

Selecting jQuery First Child without a specific class

I have a page with nested divs being used as Js/Jq tabs. When you click on a parent tab, it fires the click event of the first child underneath the parent.
$('#topRow .pricing').click(function () {
$('#secondRow .pricing').css('display', 'block');
$('#secondRow .pricing div.tab:first-child').trigger('click');
});
This functionality works great as is. However, we're adding functionality to effectively "lock" tabs by adding a "locked" class dynamically via c#/linq and then on window.ready we unbind all the tabs with the "locked" class and color them grey to indicate that they are disabled.
I'm trying to modify the last line of the jQuery code above to click the first child that DOESN'T have the "locked" class.
$('#secondRow .pricing div.tab:first-child').trigger('click');
Translating that into plainspeak, it's saying "In the Second Row, fire the click event of the first child tab in the 'pricing' group". I'd like it to say "In the Second Row, fire the click event of the first child tab in the 'pricing' group that DOES NOT have the class 'locked'".
I've tried using:
$('#secondRow .pricing div.tab:not(.locked):first-child').trigger('click');
Which seems the correct way to do this, but it still looks at the first child and just doesn't fire the trigger('click'). Am I missing something?
Here is the html:
<div id="topRow">
<div class="pricing tab" runat="server" id="pricingTop">
<div class="tabLeft">
<div class="tabMain">
Pricing
</div>
</div>
</div>
</div>
<div id="secondRow">
<div id="pricingTabGroup" runat="server" class="pricing tabGroup" style="display:none">
<div id="pricingProductA" runat="server" class="tab locked pricingProductA">
<div class="tabLeft">
<div class="tabMain">
ProductA
</div>
</div>
</div>
<div id="pricingProductB" runat="server" class="tab pricingProductB">
<div class="tabLeft">
<div class="tabMain">
ProductB
</div>
</div>
</div>
</div>
</div>
The "locked" class is added through C#:
protected void Page_Load(object sender, EventArgs e)
{
bool ProductA = //some linq query
if (!ProductA)
{
LockTab(pricingProductA);
}
protected void LockTab(HtmlGenericControl tab)
{
//Adds the "locked" class
tab.Attributes.Add("class", "locked");
}
}
Try
$('#secondRow .pricing div.tab').not('.locked').children().first().trigger('click');
Your selector looks fine though. Usually a good idea to minimize your selector complexity.
Also remember that every time you do a selector jQuery traverses the dom, so its best to save returned elements for a given selector as a variable(for performance) eg:
var $tab_containers = $('#secondRow .pricing div.tab');
And then use it multiple times like
$tab_containers.not('.locked').children().first().trigger('click');

How to hide specific jqueryui tab based on some condition?

I am working on an application in which a selected tab should not visible to specific users.My code is
<div id="tabs">
<ul>
<li>Log Tickets</li>
<li>Open Tickets</li>
</ul>
<div id="divLogTickets" runat="server" style="padding: 25px;">
</div>
</div>
if (getUserRole(Convert.ToString(Session["UserId"])) == "HR")
{
//hide tab
}
How to hide a specific tab based on specific user role.
You can add id and runat="server" attributes to the elements that you want to access from the code behind and set the .Visible property in code behind.
For example if you want to hide Log Tickets tab, here's what your aspx code should look like:
<div id="tabs">
<ul>
<li id="liLogTickets" runat="server">Log Tickets</li>
<li>Open Tickets</li>
</ul>
<div id="divLogTickets" runat="server" style="padding: 25px;">
</div>
</div>
Then set the visibility of liLogTickets and divLogTickets in code behind:
if (getUserRole(Convert.ToString(Session["UserId"])) == "HR")
{
//hide Log Tickets tab
liLogTickets.Visible = false;
divLogTickets.Visible = false;
}
You can use $(selector).hide(); method hide.
For eg:
if (getUserRole(Convert.ToString(Session["UserId"])) == "HR")
{
//hide tab
$('#userId').hide();
}
After the validations , i.e. you have validated that this particular user you want to hide it from (as you have mentioned in your code) further you can use the hide() function to hide that particular element.
$('#Id_of_Element').hide();

ASP.NET MVC4 Twitter Bootstrap Radio Button Issues

I am currently using Bootstrap V3 and MVC4.
I am having issues accessing and formatting data using bootstrap defined radio buttons.
When I declare my radio buttons using the following - Specifically data-toggle="buttons":
<div class="btn-group" data-toggle="buttons" style="padding-bottom:10px">
<label class="btn btn-primary">
#Html.RadioButtonFor(model => model.searchType, "radiobutton1") RadioButton 1
</label>
<label class="btn btn-primary">
#Html.RadioButtonFor(model => model.searchType, "radiobutton2") RadioButton2
</label>
</div>
The result is:
Everything looks perfect. When I select a button, it gets pressed until I select the other.
However, when I submit the form, the value is not passed through to the controller. It is null. I have a strongly typed view that gets values from a textbox and the radio buttons on the page.
When I define my radio buttons using the following - Specifically data-toggle="buttons-radio":
<div class="btn-group" data-toggle="buttons-radio" style="padding-bottom:10px">
<label class="btn btn-primary">
#Html.RadioButtonFor(model => model.searchType, "radiobutton1") RadioButton 1
</label>
<label class="btn btn-primary">
#Html.RadioButtonFor(model => model.searchType, "radiobutton2") RadioButton2
</label>
</div>
My result is
This is not how I want my buttons. The little circles are visible. Not to mention, once I select one of the buttons there is no turning back. I am forced to keep that one, I cannot select the other option.
The one good thing about this option is that I am able to pass in the value that is selected to the controller. When the form is submitted, I am able to see that the value of searchType in my model object is either Radiobutton1 or Radiobutton2.
And for those wondering, this is what my model looks like. It has a spot for searchType and searchString.
public class SearchForm
{
public string searchString{ get; set; }
public string searchType { get; set; }
}
What I am asking is how do I combine the 2 results?
One looks perfect but doesn't pass data, the other looks bad and passes in the date.
I have narrowed it down to the data-toggle property being set to either Buttons or Buttons-Radio.
Actually all you need to do is add a name attribute to each radio button and set it to the model item you want. Then asp.net MVC does its voodoo magic and connects the bits together.
In your case something like the code below should work:
<div class="btn-group" data-toggle="buttons" style="padding-bottom:10px">
<label class="btn btn-primary">
#Html.RadioButtonFor(model => model.searchType, "radiobutton1", new {name="searchType"}) RadioButton 1
</label>
<label class="btn btn-primary">
#Html.RadioButtonFor(model => model.searchType, "radiobutton2", new {name="searchType"}) RadioButton2
</label>
</div>
I have been able to resolve my own issue however I had to take a different approach. However I will not accept this answer as it does not toggle as it is supposed to. The button stays selected until it is out of focus.
Instead of using #Html.RadioButtonFor(...) I went back and I used buttons as my two radio options.
Since when those buttons are clicked, there is no action being taken I had to create a hidden input field, and use javascript to update its value whenever a "radio button" was clicked.
All in all here was my form code:
#Html.HiddenFor(model => model.searchType)
<div class="btn-group" data-toggle="buttons-radio">
<button type="button" id="radiobutton1" data-toggle="button" name="search" value="Radiobutton1" class="btn btn-primary">Radiobutton1</button>
<button type="button" id="radiobutton2" data-toggle="button" name="search" value="radiobutton2" class="btn btn-primary">Radiobutton2</button>
</div>
<script>
var buttons= ['radiobutton1', 'radiobutton2'];
for (var i = 0; i < btns.length; i++) {
document.getElementById(buttons[i]).addEventListener('click', function () {
document.getElementByID('searchType') = this.value;
});
}
</script>
EDIT:
I have solved the entire issue now.
I have removed my old Javascript with this jquery code:
<script type="text/javascript">
$(function () {
$("#searchDiv :button").click(function () {
$("#searchDiv :button").removeClass('active');
$(this).addClass('active');
$("#searchType").val($(this).attr("value"))
});
});
</script>
Buttons now toggle correctly. And the value is passed to the hidden field.
This seems to be a little more complicated than it needed to be, but its all good.

Categories