Insert a new section/component on drag and drop with Blazor - c#

I am trying to make a new section/div/component to appear in the droparea upon the drop action.
For example, I make two blazor components <Calculator/> and <Counter /> which should be put in a dropzone's div depending on which one was dragged. So far I have two draggable elements:
#page "/"
<!-- draggable items-->
<ul>
<li draggable="true" #ondragstart="OnDragStart">drag Counter</li>
<li draggable="true" #ondragstart="OnDragStart">drag Calculator</li>
...
</ul>
Then I have multiple divs and among them dropzone divs. Depending in which zone I drop it, there the new component should be rendered:
<!-- drop zones-->
<div class="bg-primary">
<div class="row one">Test</div>
<div dropzone="move" class="row space"
#ondrop="OnDrop" ondragover="event.preventDefault();"></div>
<div class="row two"></div>
<div dropzone="move1" class="row space"
#ondrop="OnDrop" ondragover="event.preventDefault();"></div>
<div class="row three">Test</div>
</div>
My code is not doing much yet, only indicating what action is taking place:
<div>#DragStatus</div>
#code {
public string DragStatus = "test...";
public void OnDragStart()
{
DragStatus = "started";
}
public void OnDrop()
{
DragStatus = "dropped";
}
}
How would I indicate which of two elements was taken/dragged?

I have found a solution. It is more functional than professional. Just to give an idea.
On drag start event you can pass a variable of what is being dragged to a function:
<li draggable="true" #ondragstart='(() => OnDragStart("Counter"))'>drag Counter</li>
<li draggable="true" #ondragstart='(() => OnDragStart("Calculator"))'>drag Calculator</li>
Then we need to know where this item is being dropped:
<div dropzone="move1" class="row space"
#ondrop='(() => OnDrop("first"))' ondragover="event.preventDefault();">B</div>
<div class="row two"></div>
From above we get two values {first, calculator} so it can be stored into tuple:
public List<Tuple<string, string>> myitems { get; set; }
= new List<Tuple<string, string>>();
public string place { get; set; } = "";
public string DragStatus = "test...";
Two functions will save this data only when an item is both dragged and dropped:
public void OnDragStart(string taken_a)
{
DragStatus = taken_a; //what is being dragged
}
public void OnDrop(string place_a)
{
place = place_a;
var item = new Tuple<string, string>(place, DragStatus);
myitems.Add(item);
}
Finally we can loop through a list of tuples inside dropzone div:
<div dropzone="move1" class="row space"
#ondrop='(() => OnDrop("first"))' ondragover="event.preventDefault();">
#foreach (var item in myitems)
{
#if (item.Item1 == "first")
{
#if (item.Item2 == "Calculator")
{
<Calculator />
}
#if (item.Item2 == "Counter")
{
<Counter />
}
}
}
</div>
It may work for some small data sets, because you need to put such loops under each dropzone.
Otherwise, one of the professional methods seems to be render trees. A good resource on it:
https://chrissainty.com/building-components-via-rendertreebuilder/

Related

Blazor How to get a Blazor component to not reload/re-render

I am building a dynamic Drop Down Navigation bar in Blazor. Now the problem is that when paging occurs the Navbar component reloads and the drop down dissapears (which is not what I want).
I know this is true because when I take the navigationManager.NavigateTo(route); out of the equation then it works as intended.
My MainLayout:
<div style="height: 100%; width: 100%; display: flex;">
<div style="height: 100%; width: 170px">
<NavigationMenu></NavigationMenu>
</div>
<div class="flex-child-expand">
#Body
</div>
</div>
NavigationMenu.razor
<div>
#foreach (var navButton in NavManager.MainNavButtons)
{
<div class="dropdown">
<button class="#navButton.StyleClassString" #onclick="#(() => OnButtonClicked(navButton, navButton.ButtonRoute))">#navButton.ButtonString</button>
<div class="dropdown-content">
#foreach (var button in navButton.SubSection)
{
<button class="#button.StyleClassString" #onclick="#(() => OnButtonClicked(navButton, button.ButtonRoute, button.ButtonString))">#button.ButtonString</button>
}
</div>
</div>
}
</div>
private void OnButtonClicked(NavManager.NavButton mainButtonPressed, string route, string buttonString = "")
{
if(buttonString == "")
{
foreach (var mainbtn in NavManager.MainNavButtons)
{
if (mainbtn.Section == mainButtonPressed.Section)
{
mainbtn.StyleClassString = ButtonActiveStyle.active;
}
else
{
mainbtn.StyleClassString = ButtonActiveStyle.normal;
}
//cleanup
foreach (var subButton in mainbtn.SubSection)
{
subButton.StyleClassString = ButtonActiveStyle.normal;
}
}
if(mainButtonPressed.SubSection.Count > 0)
{
mainButtonPressed.SubSection[0].StyleClassString = ButtonActiveStyle.active;
}
}
else
{
foreach (var mainbtn in NavManager.MainNavButtons)
{
if (mainbtn.Section == mainButtonPressed.Section)
{
mainbtn.StyleClassString = ButtonActiveStyle.active;
}
else
{
mainbtn.StyleClassString = ButtonActiveStyle.normal;
}
foreach (var subButton in mainbtn.SubSection)
{
if (subButton.ButtonString == buttonString)
{
subButton.StyleClassString = ButtonActiveStyle.active;
}
else
{
subButton.StyleClassString = ButtonActiveStyle.normal;
}
}
}
}
GoToPage(route);
}
private void GoToPage(string route)
{
navigationManager.NavigateTo(route);
}
*Sorry for bad indentation.
So is there a way to make the NavigationMenu.razor component from not rendering or reloading it's state when I call navigationManager.NavigateTo(route);?
To avoid a component to auto reload, you should override the ShouldRender method, and make it always return false.
However, you should check your resulting HTML. It seems that the page that you are navigating into does not inherit MainLayout.
This means that it will overwrite the
<div style="height: 100%; width: 100%; display: flex;">
<div style="height: 100%; width: 170px">
<NavigationMenu></NavigationMenu>
</div>
<div class="flex-child-expand">
#Body
</div>
</div>
portion for whatever the page contains, even if you return false in the ShouldRender.
A state change in the NavigationMenu component should not make it disappear.
NavigateTo(route) loads an entire page afresh specified by the 'route' address.
Layouts are specified at a page level. When you navigate to an address, the layout is initialized again and its UI state is reset. This means that all your dropdown expansions, formatting changes etc are lost. For example, in your case, the following CSS assignment is lost:
subButton.StyleClassString = ButtonActiveStyle.normal;
StyleClassString members of subButton(s) will be reset to the initial value (is it null?)
Therefore, the only way you can make sure that the dropdown persists its state, is if you store it somewhere.
You can achieve it in two ways:
Read it from the current URL
Store it as a state somewhere in the memory and read it in OnInitialized (complex and I won't really recommend)

Use Javascript addEventListener within Blazor component

I have a Blazor component which is rendered server-side. And I would like to have some collapsible divs inside of it. However since the code is server rendered the Javascript is not executed therefore the parts cannot collapse.
Here is the code inside my script.js file :
var coll = document.getElementsByClassName("collapsible");
var i;
for (i = 0; i < coll.length; i++) {
coll[i].addEventListener("click", function() {
this.classList.toggle("active");
var content = this.nextElementSibling;
if (content.style.maxHeight){
content.style.maxHeight = null;
} else if(window.matchMedia("(max-width:1440px)")){
// content.style.maxHeight = content.scrollHeight + "px";
content.style.maxHeight = "20vh";
}
else {
content.style.maxHeight = "50vh";
}
});
}
Here is my main.cshtml file :
<component type="typeof(Main)" render-mode="Server" />
<script src="~/js/script.js" type="text/javascript"></script>
And finally my Main component with the collapsible parts :
#using Microsoft.AspNetCore.Components;
#using Microsoft.AspNetCore.Components.Web;
<div class="collapsible">
<label for="tutu">HEADER</label>
<div id="mybtn" class="btn-rch"></div>
</div>
<div class="tutu content flex-column">
<p>CONTENT HIDDEN IN COLLAPSE</p>
</div>
<div class="collapsible">
<label for="tutu">HEADER</label>
<div id="mybtn" class="btn-rch"></div>
</div>
<div class="tutu content flex-column">
<p>CONTENT HIDDEN IN COLLAPSE</p>
</div>
<div class="collapsible">
<label for="tutu">HEADER</label>
<div id="mybtn" class="btn-rch"></div>
</div>
<div class="tutu content flex-column">
<p>CONTENT HIDDEN IN COLLAPSE</p>
</div>
#code {
}
If I use render-mode="Static" instead of render-mode="Server" it works, but since my component will have event inside of it is not a possibility for me. How can I, with the use of JSInterop for example, call my JS script to make my div collapse ?
You can do all this in Blazor. Below is a simplistic working example of what I think you are trying to achieve.
This is a collapsible div component.
CollapseDiv.razor
<div #onclick="Collapse" style="cursor:pointer;" >
<h2>#Label</h2>
</div>
#if (!Collapsed)
{
<div>#ChildContent</div>
}
#code {
[Parameter] public RenderFragment ChildContent { get; set; }
[Parameter] public RenderFragment Label { get; set; }
bool Collapsed;
void Collapse(MouseEventArgs e)
{
Collapsed = !Collapsed;
}
}
And this is the page to demo it:
Collapse.razor
#page "/collapse"
<h3>Collapse Test Page</h3>
<CollapseDiv>
<Label>I'm Collapsible</Label>
<ChildContent>
I'm the collapsed content!
</ChildContent>
</CollapseDiv>
<br />
<br />
<CollapseDiv>
<Label>I'm Collapsible Too</Label>
<ChildContent>
More collapsed content!
</ChildContent>
</CollapseDiv>
#code {
}
The key here is: Forget manipulating the DOM with Javascript, build components.
You should be able to adopt this to fit your needs.

Blazor Component not updating after StateHasChanged()

I have a Blazor Server-Side page that has the following:
#foreach (var s in OSTs)
{
<div class="alert" style="height: 30px; background:#s.Color; color: #s.TextColor;">
<div class="form-row">
<div class="col-sm-3"><strong>#s.Stage</strong><br />#s.Description</div>
<OpportunitiesStageDiv MyEditOst="#s" UpdateIt="AddOrEditStage"></OpportunitiesStageDiv>
<div class="badge badge-primary" #onclick="() => MoveItUp(s)" style="cursor: pointer; width: 6rem;"><i class=" oi oi-arrow-top"></i> Move Up</div>
<div class="badge badge-primary" #onclick="() => MoveItDown(s)" style="cursor: pointer; width: 6rem;"><i class="oi oi-arrow-bottom"></i> Move Down</div>
<div class="badge badge-primary">#s.Id - #s.StageOrder</div>
</div>
</div>
}
The Component OpportunitiesStagesDiv is as follows:
<div class="badge badge-primary" #onclick="() => EditStage(MyEditOst.Id)" style="cursor: pointer; width: 6rem;">Edit Stage</div>
<Modal #ref="#EditModal">
<Title>Edit Opportunity Stage</Title>
<Body>
<EditOpportunityStage EditOSt="#MyEditOst" Complete="AddOrEditStage"></EditOpportunityStage>
</Body>
<Footer>
<button type="button" class="btn btn-secondary" data-dismiss="modal" #onclick="() => EditModal.Close()">Close</button>
</Footer>
</Modal>
#code {
[Parameter]
public OpportunityStagesTemplate MyEditOst { get; set; }
private Modal EditModal = new Modal();
[Parameter]
public EventCallback<int> UpdateIt { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
}
private void EditStage(int SId)
{
EditModal.Open();
}
private void AddOrEditStage(int Completed)
{
if (Completed == 99)
{
}
EditModal.Close();
}
}
The MoveItUp(s) and MoveItDown(s) are functions that move the main div element in the foreach loop either up or down based on the order parameter in the query from the database. This works perfectly. What is strange is that once a DIV element is moved up or down, the edit function #onclick="() => EditStage(MyEditOst.Id)" in the component reflects as if it is the previous element.
For reference the working MoveItUp and MoveItDown functions:
private void MoveItUp(OpportunityStagesTemplate MyEditOst)
{
if (MyEditOst.StageOrder > 1)
{
OpportunitiesStagesTemplateGateway OSTGw = new OpportunitiesStagesTemplateGateway();
OpportunityStagesTemplate PreviousOst = OSTGw.GetStageByOIdAndStageOrder(MyEditOst.OId, MyEditOst.StageOrder - 1);
PreviousOst.StageOrder = MyEditOst.StageOrder;
MyEditOst.StageOrder -= 1;
OSTGw.UpdateOpportunityStage(PreviousOst);
OSTGw.UpdateOpportunityStage(MyEditOst);
ExecuteTheGetStages(LastId);
StateHasChanged();
}
}
private void MoveItDown(OpportunityStagesTemplate MyEditOst)
{
OpportunitiesStagesTemplateGateway OSTGw = new OpportunitiesStagesTemplateGateway();
int ct = OSTGw.GetCountByOId(MyEditOst.OId);
if (ct > MyEditOst.StageOrder)
{
OpportunityStagesTemplate NextOst = OSTGw.GetStageByOIdAndStageOrder(MyEditOst.OId, MyEditOst.StageOrder + 1);
NextOst.StageOrder = MyEditOst.StageOrder;
MyEditOst.StageOrder += 1;
OSTGw.UpdateOpportunityStage(NextOst);
OSTGw.UpdateOpportunityStage(MyEditOst);
ExecuteTheGetStages(LastId);
StateHasChanged();
}
}
Example I have a list as follows:
<div 1 edit moveup movedown>
<div 2 edit moveup movedown>
<div 3 edit moveup movedown>
When I move up one div
<div 1 edit moveup movedown>
<div 3 edit moveup movedown>
<div 2 edit moveup movedown>
and then click on the edit for now on line 2 for div 3, it brings up the edit as if was the div 2. It seems that the component still retains the position on the rendered UI as if DIV 2 is in on the second line and DIV 3 is still on the thirdline, although the move up and move down functions have updated the database and have updated the UI correctly to display the list in the order as it is moved. Any suggestions?
Below is sample rendered page:
Sample list picture
I have found a simple solution after careful reading of the Microsoft LifeCycle Documenttation. I found that if use the:
protected override void OnParametersSet()
{
...
}
the edit form on my modal displays the correct information. The OnParameterSet allows me to update any local component variables even if the original parameter that fed them on the OnInitialized() were set.
Thanks and I hope this answer helped all.

How to submit multiple identical forms with one button

I'm currently building and application in ASP.NET Core MVC and I have ran into a problem which I cannot solve.
I have a form for something and that form should contain multiple identical fields which are added dynamically (1-10). I have managed to do that by creating a ViewComponent which contains those form fields and I make an Ajax call to invoke the view component into a tab if a user chooses to add another segment of those fields.
function CallViewComponent(num_tabs) {
var data = { id: num_tabs };
$.ajax({
type: 'POST',
url: '/Create/CreateActivityForm',
cache: false,
data: data
}).done(function (result) {
var container = "#activity-" + num_tabs;
$(container).html(result);
});
}
The problem arises because each of those fields in that view component shares a name with the other fields so each time I invoke another view component the radio buttons are shared between all identical fields.
Here is a snippet of the ViewComponent:
#model CreateActivityViewModel
<div class="activity-tab">
<div class="form-group">
<label asp-for="La.OrdinalNumber">Redni broj aktivnosti</label><br />
<select asp-for="La.OrdinalNumber" class="ordinals" style="width:50%">
#foreach (var on in Model.OrdinalNumbers)
{
<option value="#on.Value">#on.Text</option>
}
</select>
</div>
<div class="form-group">
<label asp-for="La.Latype">Tip aktivnosti</label><br />
<select asp-for="La.Latype" class="activity-type" style="width:50%">
#foreach (var lt in Model.LaTypes)
{
<option value="#lt">#lt.LatypeName</option>
}
</select>
</div>
<div class="form-group">
<label asp-for="La.Laname">Naziv aktivnosti</label>
<input asp-for="La.Laname" type="text" name="La.Laname" placeholder="Unesite naziv aktivnosti" class="f1-activity-name form-control" id="f1-activity-name">
</div>
Here is my controller which returns the ViewComponent:
[HttpPost]
public IActionResult CreateActivityForm(int id)
{
return ViewComponent("ActivityTab", id);
}
Here is the Invoke method from the ViewComponent:
public IViewComponentResult Invoke(int id)
{
var latypes = _laTypeRepository.GetAllLaType.ToList();
var ordinals = new List<SelectListItem>();
var laperformances = _laPerformanceRepository.GetAllLaPerformance.ToList();
var teachingAids = _teachingAidRepository.GetAllTeachingAid.ToList();
var strategyMethods = _strategyMethodRepository.GetAllStrategyMethod.ToList();
var laCollaboration = _laCollaborationRepository.GetAllLaCollaboration.ToList();
for (int i = 1; i <= 100; i++)
{
ordinals.Add(new SelectListItem($"{ i }. aktivnost", i.ToString()));
}
return View( new CreateActivityViewModel
{
FormId = id,
LaTypes = latypes,
OrdinalNumbers = ordinals,
LaPerformances = laperformances,
StrategyMethods = strategyMethods,
Lacollaborations = laCollaboration,
TeachingAids = teachingAids,
TeachingAidUser = new List<TeachingAid>(),
TeachingAidStudent = new List<TeachingAid>()
});
}
And finally this is where the ViewComponent gets invoked. It is inside another form because I need to submit the main form and all the ViewComponents at once:
<fieldset>
<h4>Aktivnosti</h4>
<!-- Activity Tabs -->
<div id='activity-tabs'>
<!-- Activity Links -->
<ol id="#activity-links">
<li><a href='#activity-1'>#1</a></li>
<li id="add-activity"><button type="button" id='add-activity'><i class="fa fa-plus"></i></button></li>
</ol>
<!-- Activity Content -->
<div id='activity-1'>
<h3>Aktivnost #1</h3>
#await Component.InvokeAsync("ActivityTab")
</div>
</div>
<!-- Navigation Buttons -->
<div class="f1-buttons">
<button type="button" class="btn btn-previous">Prethodna</button>
<button type="submit" class="btn btn-submit">Kreiraj scenarij</button>
</div>
</fieldset>
My question is how do I separate those identical forms and be able to submit them and store every single one of those forms into an array of objects which I can then store into a database.
I am open to all ideas and will change the entire code if necessary.
Thank you!
If you have an array of objects you need to render the components using a FOR loop rather than a FOR-EACH. I like to push common code into a shared view but you can code direct in the view. You will need to set your asp-for attributes in order to bind values to the model
#for (int index = 0; index < Model.Resources.Count; index++)
{
<partial name="_ResourceHidden" for="#Model.Resources[index]" />
Direct render
#for (int index = 0; index < Model.Resources.Count; index++)
{
<tr>
<td>
#Model.Resources[index].ResourceName
</td>

umbraco and c# Razor : how to print odd and even vlaue alternativelty in partialview or view in page from Model

I have two html for left side image & content and for right side image & content. value print from models. what can i write in for-each loop to print odd and even value alternatively in partial view in page from Model.
#inherits UmbracoViewPage<List<ProjectName.Models.DealerSectionModel>>
#using ProjectName.Models;
#using Umbraco.Core;
#{
foreach (DealerSectionModel historylist in Model) // what should i write in this loop .(ex if 1 value in model then print odd html)
{
if ()
{
#RenderEvenHistoryList(historylist)
}
else
{
#RenderOddHistoryList(historylist)
}
}
}
#helper RenderOddHistoryList(DealerSectionModel item)
{
<div class="row history-second-section py-2 py-xl-4 py-lg-4 py-md-4 py-sm-2">
<div class="col-12 col-xl-6 col-lg-6 col-md-6 col-sm-12 history-second-images">
<div class="quote my-3"><iframe src="#item.VideoUrl" width="510" height="282" frameborder="0" allowfullscreen></iframe></div>
</div>
<div class="col-12 col-xl-6 col-lg-6 col-md-6 col-sm-12 history-second-content">
<div class="content-year">#item.Title </div>
<h4>#item.ImageTitle</h4>
<p>#item.ImageDescription</p>
</div>
</div>
}
}
#helper RenderEvenHistoryList(DealerSectionModel item)
{
// render html for even model value
}
}
I don't know much about umbraco, but why not try something simple like
#{
var even = false;
foreach (DealerSectionModel historylist in Model) // what should i write in this loop .(ex if 1 value in model then print odd html)
{
if (even)
{
#RenderEvenHistoryList(historylist)
}
else
{
#RenderOddHistoryList(historylist)
}
even = !even;
}
}
You can do like this
#{ int count = 1;
foreach (DealerSectionModel historylist in Model)
{
if (count % 2 == 0)
{
#RenderEvenHistoryList(historylist)
}
else
{
#RenderOddHistoryList(historylist)
}
count++;
}
}

Categories

Resources