I just started to have a look in blazor (v0.3) and doing some test I wanted to add a list using blazor
First I created a List<string> to test a simple list in the same page
<ul>
#foreach (var item in listItems)
{
<li>#item</li>
}
</ul>
#functions {
private List<string> listItems = new List<string>();
private string newItem;
private void AddItem()
{
if (string.IsNullOrEmpty(newItem))
return;
listItems.Add(newItem);
newItem = "";
}
}
this is working fine, is adding every element to the list when I add it. but then, i tried to add components, add a single component was easy, based on this question here but for a list I had the next problem:
I created a <li> compontent just to test the functionality of components, here is the component view
<li id="#ID">
#Text
</li>
#functions {
[Parameter]
string Text { get; set; }
[Parameter]
string ID { get; set; }
}
then in the parent view
<input type="text" bind="TxtExample" name="inpAdd"/>
<button onclick="#addCompoment">add comp1</button>
<div class="simple-list-list">
#if (!componentListTest.Any())
{
<p>You have no items in your list</p>
}
else
{
<ul>
#foreach (var item in componentListTest)
{
#item
}
</ul>
}
</div>
#functions {
private List<RenderFragment> componentListTest { get; set; }
private int currentCount {get; set;}
private string TxtExample { get; set; }
protected override void OnInit()
{
currentCount = 0;
componentListTest = new List<RenderFragment>();
}
protected void addCompoment()
{
componentListTest.Add(CreateDynamicComponent(currentCount));
currentCount++;
}
RenderFragment CreateDynamicComponent(int counter) => builder =>
{
var seq = 0;
builder.OpenComponent(seq, typeof(listExample));
builder.AddAttribute(++seq, "Text", "text -- "+TxtExample);
builder.AddAttribute(++seq, "id","listed-"+counter);
builder.CloseComponent();
};
}
when I load the fist element is loaded correctly:
but when I entered the second one, all of them are replaced for the last one:
Any idea whats going on?
You are making it too complicated. You don't need to dynamically instantiate components for this scenario to work.
You can just do a:
<ul>
#foreach (var item in listItems)
{
<myComponent bind-myVar="#item"></myComponent>
}
</ul>
And the components will be instantiated for you.
Also see here how to make the parameters work on your component.
This is because TxtExample is global to the component. When Blazor detects a potential UI change, it recalculates the entire component and updates the DOM with any differences. So when you change the textbox, TxtExample is updated and then the Razor is recalculating, inserting the new value of TxtExample for all rows.
Related
I have a simple collection of "Conversation" like this:
public class Conversation
{
public string? contactName { get; set; }
public string? thread { get; set; }
public DateTime newestMsg { get; set; }
}
I was originally using #foreach to put these onto a page to display as a scrollable list. I'd load 30 of them from an API call and populate them into "ConversationList":
#foreach (var conv in ConversationList)
{
<div class="conversation">
<div>#conv.contactName</div>
<div>#conv.thread</div>
<div>#conv.newestMsg</div>
</div>
}
#code {
Conversation[]? ConversationList = new Conversation[] { };
}
I saw Blazor has the Virtualize component which would let me have tons of conversations be scrollable and load on demand, so I changed my code to use it:
<Virtualize ItemsProvider="#LoadConversations" Context="conv">
<ItemContent>
<div class="conversation">
<div>#conv.contactName</div>
<div>#conv.thread</div>
<div>#conv.newestMsg</div>
</div>
</ItemContent>
</Virtualize>
#code {
private async ValueTask<ItemsProviderResult<Conversation>> LoadConversations(ItemsProviderRequest request)
{
string apiURL = "https://myAPI:00000?start=" + request.StartIndex + "&limit=" + request.Count;
ConversationRequest? convRequest = await Http.GetFromJsonAsync<ConversationReq>(apiURL);
if (convRequest != null && convRequest.success)
{
return new ItemsProviderResult<Conversation>(convRequest.Conversations, request.StartIndex + request.Count + 4);
}
else
{
return new ItemsProviderResult<Conversation>(null, 0);
}
}
}
In my first attempt, I could select a specific item in "ConversationList" and modify it's contents, having the state change on the page. For example, if a new message came in from SignalR, I could select a Conversation by it's "thread" property and update the "newestMsg" datetime.
Now that I have them Virtualized instead, it's not clear how to do this. I can't use Virtualize's "ItemsProvider" and "Items" at the same time. I want to keep the ItemsProvider for the sake of automatic loading while scrolling, knowing the StartIndex and Count automatically.
Hi i try to create a search function, showing my data via blazor components.
The search function works fine, and return the result i want. But it does not update the old component. So if my search return 1 result, then the component only update the title, but other data from my model, will stay from the first loaded card. If search for an extra letter so item not exist, all the component will disapeer. But when i delete the last again, the component will show up again, with the right data.
<input type="search" #bind-value="SearchValue" #bind-value:event="oninput">
<div class="row justify-content-center mb-5">
#foreach (var item in FoundItems)
{
<ItemCard ItemDTO=item />
}
</div>
#code
public string SearchValue { get; set; } = "";
private List<ItemDTO> Items { get; set; }
protected async override Task OnInitializedAsync()
{Items = await _http.GetFromJsonAsync<List<ItemDTO>>("api/item");}
{
List<ItemDTO> FoundItems => Items.Where(i =>
i.Description.ToLower().Contains(SearchValue.ToLower())).ToList();
}
I solved it allready.
In my component i added.
protected override void OnParametersSet()
{
StateHasChanged();
}
I work on an ASP.NET MVC project that I need some help with. I need to be able to create x number of textboxes when the user click "add textbox". When the user enter the page a viewmodel is loaded. This viewmodel need to handle the x number of textboxes that the user create when he is on the page so that when the page is posted these textboxes are part of the model. The model should look something like this..
public class PlanViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public List<EventViewModel> EventList { get; set; } // this should be the list of textboxes that the user "create" by clicking add new
}
public class EventViewModel
{
public string Name { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public string Description { get; set; }
}
I'm kinda lost on how to do this so any help is appreciated.
UPDATE
I've added this javascript that add textboxes client side..
<script type="text/javascript">
function GetDynamicTextBox(value) {
return('<input type="text" name="events[0].Key" value="box1" /><input type="text" name="events[0].Value.StartDate" value="box2"/><button type="button" class="btn btn-sm btn-primary" onclick="RemoveTextBox(this)"><i class="fa fa-angle-right"></i> Remove</button>');
}
function AddTextBox() {
var div = document.createElement('DIV');
div.innerHTML = GetDynamicTextBox("");
document.getElementById("divcontent").appendChild(div);
}
function RemoveTextBox(div) {
document.getElementById("divcontent").removeChild(div.parentNode);
}
</script>
<div id="divcontent" class="form-group">
<button type="button" class="btn btn-sm btn-primary" onclick="AddTextBox()"><i class="fa fa-angle-right"></i> Add</button>
</div>
I think I only need to add unique id's for the textboxes like this...
events[0].Key
events[1].Key
events[2].Key
and so on..
But I don't know how. Anyone knows?
You can add a list of String, like this
public String[] MyTextFields
and then create HTML using Javascript, like this:
<input name="myTextFields[0]"></input>
<input name="myTextFields[1]"></input>
In Razor view:
#for (var i = 0; i < Model.EventList.Count; i++)
{
#Html.EditorFor(x => Model.EventList[i].Name)
}
To set the name attribute of all edited elements in javascript, this is to be called on page load, and any time the collection changes (item is added or removed):
var children = document.getElementById("myDIV").children; // TODO: here supposing children are the input elements, may be different on your page (they may be nested in a different way)
for (i = 0; i < children.length; i++)
{
var el = children[i];
el.name = 'EventList[' + i + '].Name'; // TODO: look into source code of the generated page for the actual format of existing elements and then modify the mask accordingly
el.id = 'EventList[' + i + '].Name';
}
If it is ok to have JS dependency than I suggest to use light Knockout library. It will help you to create/edit/delete your inputs. Check example in JS fiddle.
Use HTML to adjust your view. Tag data-bind lets you to bind to data and events
<button data-bind="click: addInput">Add</button>
<div data-bind="foreach: inputs">
<input data-bind="value: text"/><br />
</div>
<button data-bind="click: proceed">Proceed</button>
<!-- Use Knockout JS library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.0/knockout-min.js"></script>
Then small JS script which handles adding new input and processing data on click.
function InputData(text) {
let self = this;
self.text = text;
}
function InputViewModel() {
let self = this;
// Your array of HTML inputs
self.inputs = ko.observableArray([new InputData("Default value")]);
self.output = ko.observable();
self.addInput = function() {
// Dynamically adds new input on user click button "Add"
self.inputs.push(new InputData(""));
};
self.proceed = function() {
// Process all input with their values
for (var i = 0; i < self.inputs().length; i++) {
console.log(self.inputs()[i].text);
}
}
}
// Bind our JS to HTML view
ko.applyBindings(new InputViewModel());
I am creating the survey application for which I am creating the controls dynamically(Like checkbox,radio-button,textbox etc).
Each question will have the controls depending upon the control type assigned to the question and on question type the answer choices(checkbox, radio button) will be rendered.
On Next/Previous navigation I am storing current page answers in the database. While navigating the page I am doing ajax call for database saving and my UI/controls are NOT in the form.
I have created ViewModel based on my LMS_SurveyQuestions and LMS_SurveyQuestionOptionChoice table.
So, while creating the UI in view in for loop, I have directly assigned SurveyQuestionOptionChoiceID as Control ID while creating the AnswerChoice controls and stored the same in the table SurveyUserAnswer table.
Model
public class LMS_TraineeSurveyPaginationViewModel
{
public List<LMS_SurveyQuestions> SurveyQuestions { get; set; }
public List<LMS_SurveyQuestionOptionChoice> SurveyQuestionOptionChoice { get; set; }
public SurveyPager Pager { get; set; }
}
and this is how I rendered the view
#foreach (var item in Model.SurveyQuestions)
{
foreach (var data in Model.SurveyQuestionOptionChoice.Where(x => x.SurveyQuestionID == item.SurveyQuestionID).ToList())
{
if (item.QuestionTypeID == QuestionType.RadioButton)
{
<li style="list-style:none;">
<input type="radio" name="rb" id="#data.SurveyQuestionOptionChoiceID" />
<span>#data.OptionChoice</span>
</li>
}
else if (item.QuestionTypeID == QuestionType.CheckBox)
{
<li style="list-style:none;">
<input type="checkbox" id="#data.SurveyQuestionOptionChoiceID" name="#data.SurveyQuestionOptionChoiceID" " />
<span>#data.OptionChoice</span>
</li>
}
}
}
and while saving the answer into database I have created the JSON/JS array as model for SurveyUserAnswer and saved it into database as follows. Below is example for radio button
function SaveValues() {
var surveyQuestion = #Html.Raw(Json.Serialize(Model.SurveyQuestions.ToArray()));
var surveyQuestionOptionChoide = #Html.Raw(Json.Serialize(Model.SurveyQuestionOptionChoice.ToArray()));
for (item in surveyQuestion) {
var surveyQuestionID=surveyQuestionViewModel[item].SurveyQuestionID;
var filteredData = surveyQuestionOptionChoide.filter(function(filteredItem) {
return (filteredItem.SurveyQuestionID==surveyQuestionID);
});
for (optionChoice in filteredData) {
if(surveyQuestion[item].QuestionTypeID=='#QuestionType.RadioButton') {
if (($('#'+SurveyQuestionOptionChoiceID).prop("checked"))) {
surveyUserAnswer.push({ SurveyUserAnswerID: filteredData[optionChoice].SurveyUserAnswerID==null?0:filteredData[optionChoice].SurveyUserAnswerID,
SurveyQuestionOptionChoiceID: SurveyQuestionOptionChoiceID,SurveyUserID:'#ViewBag.SurveyUserID',AnswerText:null,
MarksObtained:filteredData[optionChoice].Marks,WhenCreated:'#DateTime.UtcNow',WhoCreated:'#tenant.UserID'});
}
}
}
}
$.post('#Url.Action("GetTraineeSurvey", "Survey")', {SurveyID:surveyID,page:page, surveyUserAnswer: surveyUserAnswer,PrevBranchQuestionPage:currentPage,IsBranchQuestionAvailable:IsBranchQuestionAvailable }, function (data) {
$('#surveyModalContent').html('');
$('#surveyModalContent').html(data);
$("#surveyModal").modal('show');
}).fail(function() {
alert( "error in GetTraineeSurvey" );
}).success(function() { });
}
So, my question is how can I validate dynamically created controls in this scenario ?
You can use Unobtrusive jQuery validation basic on data attributes on controls.
Read more about that on this LINK.
I have there 6 UL and inside each UL i dynamically generate li from serverside. right now for 6 UL i am doing ti 6 times repetadly. Is there a way i can create a funciton to pass ul as an parameter in the function and call same function with different ul elements.
right now i have this
<ul ID="ul_1"></ul>
<ul ID="ul_2"></ul>
<ul id="ul_3"></ul>
<ul id="ul_4"></ul>
server side code to populat is like this
foreach (String li in ListA)
{
HtmlGenericControl uli = new HtmlGenericControl("li");
uli.InnerHtml = li;
ul_1.Controls.Add(uli);
}
I am using this code for each of the UL that is 4 times but i am trying to create a function so that i can use the same function just passing the UL id. I am not having any idea...Any help folks....
If I correctly understood what you want maybe you could try something like this.
Model:
public class ListItem {
public string Content { get; set; }
}
public class ListOfItems {
public int Id { get; set; }
public IList<ListItem> Items { get; set; }
}
View (for example using razor):
#model IEnumerable<ListOfItems>
....
#foreach (var itemList in Model)
{
<ul id="ul_#itemList.Id">
#foreach(var item in itemList.Items) {
<li>#item.Content</li>
}
</ul>
}
Controller:
public ActionResult ViewList(){
var model = new List<ListOfItems>();
var listItem1 = new ListItem
{
Content = "My first list item!"
};
var listItem2 = new ListItem
{
Content = "My second list item!"
};
var listOfItems1 = new ListOfItems
{
Id = 1
};
listOfItems1.Item.Add(listItem1);
listOfItems1.Item.Add(listItem2);
model.Add(listOfItems1);
return View(model);
}
A little bit of refactoring and you're good to go :)