Could not set or bind model property with Bootstrap Datepicker in Blazor - asp.net-core

I am using bootstrap datepicker and the problem is that when I pick a date, it does not fire a change or input event and noting is binding with the model property Course.StartDate or Course.EndDate.
The default datepicker works but does not support Afghanistan datetime. That is why I use boostrap datepicker.
Blazor code:
#using Microsoft.AspNetCore.Mvc.Rendering
#using myproject.Data
#using Microsoft.JSInterop;
#inject myproject.Repository.CoursesRepository _coursesRepository
#inject IJSRuntime JS
<EditForm Model="#Course" OnValidSubmit="e=> { if(selectedId == 0) { addCourse(); } else { updateCourse(Course.CourseId); } }">
<div class="mb-2">
<div>#Course.StartDate</div>
<label class="col-form-label" for="StartDate">#Loc["Start Date"]<span class="text-danger fs--1">*</span>:</label>
<InputDate class="form-control" #bind-Value="Course.StartDate" #bind-Value:format="yyyy-MM-dd" id="StartDate" />
<ValidationMessage class="text-danger" For="(() => Course.StartDate)"/>
</div>
<div class="mb-2">
<label class="col-form-label" for="EndDate">#Loc["End Date"]<span class="text-danger fs--1">*</span>:</label>
<InputDate class="form-control" #bind-Value="Course.EndDate" #bind-Value:format="yyyy-MM-dd" id="EndDate"/>
<ValidationMessage class="text-danger" For="(() => Course.EndDate)"/>
</div>
</EditForm>
#code {
public CourseModel Course = new();
public string[] dates = new string[] { "#StartDate", "#EndDate" };
protected override void OnAfterRender(bool firstRender)
{
base.OnAfterRender(firstRender);
loadScripts();
}
void addCourse()
{
_coursesRepository.AddCourse(Course);
FillData();
Course = new();
var title = "Course";
Swal.Success(title : Loc[$"{title} added successfully"],toast : true);
}
// initializes the datepicker
public async Task loadScripts()
{
await JS.InvokeVoidAsync("initializeDatepicker", (object) dates);
}
}
This is script for initializing the datepickers
<script>
function initializeDatepicker(dates) {
dates.forEach((element) => {
$(element).datepicker({
onSelect: function(dateText) {
// this is not working
element.value = this.value;
/*
tried this and still not working
$(element).trigger("change");
also tried this and still not working
$(element).change();
*/
// this is working
console.log("Selected date: " + dateText + "; input's current value: " + this.value);
},
dateFormat: 'yy-mm-dd',
changeMonth: true,
changeYear: true
});
});
}
</script>

The reason for this is that the changes are made with JavaScript and so the page state does not change for Blazor, in other words, Blazor does not notice the value change at all.
To solve this problem, you must inform the Blazor component of the changes by calling a C# method inside the JavaScript function. For this, you can use the DotNet.invokeMethodAsync built-in dotnet method. As follows:
DotNet.invokeMethodAsync('ProjectAssemblyName', 'ComponentMethod', this.value.toString())
Its first argument is the assembly name of your project. The second argument is the name of the C# function that you will write in the component, and finally, the third argument is the selected date value.
The method called in C# should be as follows:
static string selectedDate;
[JSInvokable]
public static void ComponentMethod(string pdate)
{
selectedDate = pdate;
}
This method must be decorated with [JSInvokable] and must be static.
I have done the same thing for another javascript calendar in Persian language. Its codes are available in the JavaScriptPersianDatePickerBlazor repository.
You can also create a custom calendar in the form of a component so that you can use it more easily in all components in any formats that you want such as DateTime or DateTimeOffset or string and so on. There is an example of this in the AmibDatePickerBlazorComponent repository.

Related

Spinner does not show b/c the bound variable is not updated

I'm working on a Blazor server side app. The page has a table with a list of cars and some filter elements on top. When I select a filter, a spinner should be visible until the new data is fetched and rendered.
The spinner with its variable:
<div class="spinner-border #spinner" role="status">
<span class="visually-hidden">Loading...</span>
</div>
#code{
string spinner = "invisible";
public string vehicleTypeFilter
{
set
{
_vehicleTypeFilter = value;
ApplyFilters();
}
get { return _vehicleTypeFilter; }
}
}
The select for the Baumuster (vehicleType) is bound to the vehicleTypeFilter variable:
<div class="col-md-2 form-floating">
<select class="form-control" #bind="vehicleTypeFilter">
<option value="" selected>Alle</option>
#foreach (var vehicleType in vehicleTypes.OrderBy(x => x.Description))
{
<option value="#vehicleType.Description">#vehicleType.Description</option>
}
</select>
<label>Baumuster</label>
</div>
Then a value is selected, the ApplyFilter method is triggered through the setter of the vehicleTypeFilter variable:
public void ApplyFilters()
{
ToggleSpinner();
// I also tried a StateHasChanged(); right here
// 1. Get all cars
cars = model.CreateIndexViewModel();
// 2. Filter for Baumuster / vehicle type
if (!string.IsNullOrEmpty(vehicleTypeFilter))
{
cars.viewModels = cars.viewModels.Where(x => x.VehicleDescription == vehicleTypeFilter).ToList();
}
ToggleSpinner();
}
The ToggleSpinner method:
public void ToggleSpinner()
{
if (spinner == "invisible" )
spinner = "";
else
spinner = "invisible";
}
Unfortunately, I don't see the spinner. When I inspect the html page right after the breakpoint hits the Baumuster-filter, the value of spinner is still set to "invisible". I even tried to call StateHasChanged(); after the first ToggleSpinner() but that didn't help.
You've shown a lot of code, but I don't see ToggleSpinner
However, you call it twice in your ApplyFilters method, with no blocking calls, so I'd assume that it's turning the spinner on and off so fast that it doesn't render (or at least that you can't notice it).
If the methods you call in ApplyFilters actually take any time, then Henk's got the right idea-- except you should use async Task I think.
Your problem is that you want async behaviour from a synchronous property. The standard advice is against async void but if you want to stay with the property, the minimal change would be:
public async void ApplyFilters()
{
ToggleSpinner();
// I also tried a StateHasChanged(); right here
StateHasChanged(); // this _requests_ an update
await Task.Delay(1); // this is why you need async void
... as before
ToggleSpinner();
StateHasChanged();
}

livewire view don't send any xhr request to component

I have a select html tag in livewire view:
<select wire:model="categoryId" wire:change="$emit('attr_cat', $event.target.value)" name="category_id" class="form-control">
<option value="0">Select category</option>
#foreach($categories as $category)
<option value="{{$category->id}}">{{ $category->name }}</option>
#if(count($category->childs))
#include('admin.categories.subcategorylist',['childs' => $category->childs])
#endif
#endforeach
</select>
and a livewire component with following code:
namespace App\Http\Livewire\Admin;
use App\Models\Attribute;
use App\Models\Category;
use App\Models\CategoryAttribute;
use Barryvdh\Debugbar\Facade as Debugbar;
use Livewire\Component;
class CategoryAttributes extends Component
{
public $attributeId;
public $categoryId;
public $attributeCategories;
public $categories;
protected $listeners = ['attr_cat' => 'addAttributeToCategory'];
public function mount()
{
$this->attributeCategories = Attribute::find($this->attributeId)->categories()->get();
$this->categories = Category::whereNull('category_id')->orderBy('name')->with('childs')->get();
$this->categoryId = 0;
}
public function render()
{
return view('livewire.admin.category-attributes');
// return view('livewire.admin.cat-attr-test');
}
public function addAttributeToCategory($value){
Debugbar::addMessage($value);
CategoryAttribute::create([
'category_id' => $this->categoryId,
'attribute_id' => $this->attributeId,
]);
}
public function updatedCategoryId(){
Debugbar::addMessage($this->attributeId);
}
}
and mount livewire component with this line in my blade view:
#livewire('admin.category-attributes', ['attributeId' => $attribute->id], key('attr-'.$attribute->id))
but, when I change selected item, nothing happens and no xhr requests are sent to the server by the browser.
Let me say this too, I have inserted #livewireStyles and #livewireScripts in master blade file and they are loaded correctly.
Thank you for your help.
I found the solution to my problem. I had to put all the contents of the view in a standalone tag so that livewire could assign an ID to it. Otherwise, livewire will refuse to send any request to the server. 

Optional query string parameters in ASP.NET Razor pages

In my advanced search form I have around 25 fields. When I submit the form all parameters are passed to the URL even when null. How may I pass the parameters which have only been changed by the user and have a value? All 25 fields are optional.
I have around 25 of these:
[BindProperty(SupportsGet = true)]
[Display(Name = "Foo")]
public int? Foo{ get; set; }
OnGetAsync:
public async Task<IActionResult> OnGetAsync()
{
Properties = await _DarkMatterRepo.FindAsync(Foo, ...)
return Page();
}
How may I pass the parameters which have only been changed by the user and have a value?
To achieve the above the requirement, you can try to check form data and navigate to expected page, like below.
Code of form
<form method="get">
<input type="text" name="name">
<input type="email" name="emailaddress">
<input type="number" name="age">
#*other input fields*#
<input type="submit">
</form>
Dynamically generate QueryString and navigate to specific page
<script>
$( "form" ).on( "submit", function( event ) {
event.preventDefault();
//console.log($(this).serializeArray());
var formdata = $(this).serializeArray();
var parm = "";
$.each(formdata, function (index, item) {
if (item.value!="") {
parm += item.name + "=" + item.value + "&";
}
});
//console.log(parm.substring(0, parm.length - 1));
window.location.href = "#Url.Page("Index")" + "?" + parm.substring(0, parm.length - 1);
});
</script>

Aurelia compose bind.two-way not working

I am trying to render my aurelia view dynamically, using compose within repeater and it is working fine but my two way binding not working. The view that is getting rendered using compose element doesn't update the property of parent view model.
my code for parent view js file is
export class Index {
public _items: interfaces.IBaseEntity[];
public data: string;
constructor() {
this._items = new Array<interfaces.IBaseEntity>();
this._items.push(new Address());
this._items.push(new HomeAddress());
}
activate() {
this._items.forEach((entity, index, arr) => {
entity.init();
});
//this.data = "data";
}
}
my parent html is as below. In this html i got custom element on which my two binding works but not with compose
<template>
<require from="form/my-element"></require>
<div repeat.for="item of _items">
<!--<my-element type.two-way="data" model.two-way="item.model"></my-element>-->
<compose view-model="${item.view}" model.two-way="item.model"></compose>
</div>
</template>
My child view model
import * as interfaces from '../interfaces';
import {useView, bindable} from 'aurelia-framework';
export class Address implements interfaces.IBaseEntity {
public view: string = "form/address";
#bindable model: string;
constructor() {
console.log("address constructed - " + this.model);
}
init = (): void => {
this.model = "Address";
}
activate(bindingContext) {
this.model = bindingContext;
console.log("address ativated - " + this.model);
}
}
and child view html is
<template>
<h2>Address Template</h2>
<input type="text" value.two-way="model" class="form-control" />
</template>
I know the issue now. I am passing simple property into my compose which doesn't gonna work. It has to be an object

using MVC4 Strongly typed view with Knockout

I am trying to use knockout with MVC strongly typed view. Since my model will have over 20 properties, I prefer to use strongly-typed view model to post back data by using ko.mapping.toJS and ko.Util.postJson. The Eligible field was passed back correctly, however the following code does not post back the selected option from drop down list, it just showed value as 0 when I looked that selectOptionModel on the controller. Can someone point out what I did wrong?
the view model from server side is as follows:
public class SelectOptionModel
{
public bool Eligible { get; set; }
public int selectedOption { get; set; }
public IEnumerable<SelectListItem> AvailableOptions
{
get
{
return Enum.GetValues(typeof(OptionEnum)).Cast<OptionEnum>()
.Select(x => new SelectListItem
{
Text = x.ToString(),
Value = x.ToString()
});
}
}
}
public enum OptionEnum
{
[Description("First")]
FirstOption = 1,
[Description("Second")]
SecondOption = 2,
[Description("Third")]
ThirdOption = 3
}
The razor view is like following:
#model TestKo.Models.SelectOptionModel
...
subViewModel = ko.mapping.fromJS(#Html.Raw(Json.Encode(Model)));
...
}
#using (Html.BeginForm()){
<button type="submit" class="button" id="SaveBtn">Save</button>
<div data-bind="with:vm">
<div>
#Html.LabelFor(model => model.Eligible)
#Html.CheckBoxFor(model => model.Eligible, new { data_bind = "checked: selectOptionVM.Eligible" })
</div>
<div>
#Html.LabelFor(model => model.selectedOption)
#Html.DropDownListFor(model => model.selectedOption, Model.AvailableOptions,
new
{ data_bind = "options: selectOptionVM.AvailableOptions, optionsText: 'Text', optionsValue: 'Value', value: selectOptionVM.selectedOption"
})
</div>
</div>
}
The javascript for the knockout view model is:
sectionVM = function (data) {
var self = this;
var selectOptionVM = data;
return {
selectOptionVM: selectOptionVM
}
}
$(document).ready(function () {
var viewModel = {
vm: new sectionVM(subViewModel)
};
ko.applyBindings(viewModel);
$("#SaveBtn").click(function () {
var optionModel = ko.toJS(viewModel.vm.selectOptionVM);
ko.utils.postJson($("form")[0], optionModel)
});
});
The controller part:
[HttpPost]
public ActionResult Create(SelectOptionModel selectOptionModel)
{
try
{
// TODO: Add insert logic here
var modelSaved = selectOptionModel;
return RedirectToAction("Index");
}
catch
{
return View();
}
}
I'm venturing a bit of a guess here, but this could be the problem: the id-bit of your selected option will always be a string (because it will go in the <option value="" attribute). Your endpoint expects an int. As far as I can see, you don't convert the selectedOption before sending it to the server. try parseInt(selectedOption, 10) before sending it to the server. Also, use the network tool in your browser to debug the info that is being sent to the controller. That might help you to zone in on the problem.
Actually it works. Somehow it was not working previously, but after I cleared cache, cookies etc, it just worked. Thanks everyone!