Want to make navigation dynamic in MainLayout (Blazor.net) - layout

Fail to make navigation in Blazor.net (Core 3.0 alpha 6) dynmic (loaded by Http Get Method)
I tried to include in "MainLayout.razor" file of default blazor project template the following dynamic loop.
the #body content was loaded correctly but navigation menu remains empty...
#inherits LayoutComponentBase
#using Pegasus.Shared
#inject HttpClient Http
<div class="sidebar">
#if (categories == null)
{
<p><em>Loading...</em></p>
}
else
{
#foreach (var category in categories)
{
<div>#category.Name</div>
}
}
</div>
<div class="main">
<div class="top-row px-4">
About
</div>
<div class="content px-4">
#Body
</div>
</div>
#code{
gb_Category[] categories;
protected override async Task OnInitAsync()
{
Console.WriteLine("Test");
var storeId = new Guid("c72d9757-98d5-4da2-89ea-2ffe44490162");
categories = await Http.GetJsonAsync<gb_Category[]>($"api/Category/actives/{storeId}");
}
}
No error message but empty navigation menu and I don't know why exaclty right now...
Same code runs if I put it into #page "/" but within #body of the MainLayout...
Goal is to load navigation items dynamic via Http and afterwards navigate through main data (#page's).
Thx

Related

Blazor, how can I change a component in MainLayout when I change page

I want to add a toolbar inside website, the toolbar change inside component on each page. For now, I have this but I want my toolbar to be like this. How could i make this toolbar to update depend on the page the user go ?
The toolbar would be in the MainLayout and need to change content with a switch (not the best option I think) or is it possible to give new content to MainLayout from the page content ?
This is the code for the banner component :
<div class="extend-space" style="left:#($"-{Convert.ToInt32(offsetX)}px")">
<div class="banner" style="left:#(Convert.ToInt32(offsetX)+"px");width:#(Convert.ToInt32(width)+"px");">
<div class="banner-title">
#if (Icon != null)
{<i id="banner-title-icon" class="icon fas fa-#Icon"></i>}
<h3 class="title">#Title</h3>
</div>
<div class="toolbar">
<span id="arrow-left" class="scrollable" onclick="lastTool()">
<i class="fas fa-angle-left arrow"></i>
</span>
<span id="toolbar">
#ChildContent
</span>
<span id="arrow-right" class="scrollable" onclick="nextTool()">
<i class="fas fa-angle-right arrow"></i>
</span>
</div>
</div>
ChildContent should be a list of buttons with function onclick on it so this is the part that need to update on each page.
I add an example of how I use it on a page :
<XLBanner Title="Catégories" Icon="sitemap">
<XLButton Icon="plus" Content="#SharedLocalizer["Add"]" OnClickFunction="#AddCategorie" />
<XLButton Icon="save" Content="#SharedLocalizer["Save"]" OnClickFunction="#Save" disabled="#(!UnsavedChanges)" />
<XLButton Icon="redo" Content="#SharedLocalizer["Reset"]" OnClickFunction="#DeleteUnsavedChanges" disabled="#(SelectedCategorie == null)" />
<XLButton Icon="trash-alt" Content="#SharedLocalizer["Remove"]" OnClickFunction="#SuppCategorie" disabled="#(SelectedCategorie == null)" />
<XLButton Icon="copy" Content="#SharedLocalizer["Copy"]" OnClickFunction="#CopyCategorie" disabled="#(SelectedCategorie == null)" />
<XLButton Icon="download" Content="#SharedLocalizer["Export"]" OnClickFunction="#Export" /
</XLBanner>
What would be needed to update is the XLButton and the OnClickFunction.
My banner has differents tools depend on the page exemple dashboard page, exemple categorie page
If I understand the question correctly a version of this should work for you.
Basically:
Create a simple service that holds the menu data and has an event that is raised whenever the menu data changes and register it.
Use a DynamicComponent in Layout that plugs into the service.
Trigger StateHasChanged on the Layout whenever the service raises a menu change event.
Set the menu you want in each page in OnInitialized.
Two "Menus" to work with:
Menu1.razor
<h3>Menu1</h3>
Menu2.razor
<h3>Menu2</h3>
A simple LayoutService
public class LayoutService
{
public Type MenuControl { get; private set; } = typeof(Menu1);
public Dictionary<string, object>? MenuParameters { get; private set; }
public event EventHandler? MenuChanged;
public void ChangeMenu(Type menu)
{
this.MenuControl = menu;
MenuChanged?.Invoke(this, EventArgs.Empty);
}
public void ChangeMenu(Type menu, Dictionary<string, object> parameters)
{
this.MenuParameters = parameters;
this.MenuControl = menu;
MenuChanged?.Invoke(this, EventArgs.Empty);
}
}
registered in Program.cs:
builder.Services.AddScoped<LayoutService>();
MainLayout.razor
#inherits LayoutComponentBase
#inject LayoutService LayoutService;
#implements IDisposable
<PageTitle>BlazorApp1</PageTitle>
<DynamicComponent Type=this.LayoutService.MenuControl Parameters=this.LayoutService.MenuParameters />
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
About
</div>
<article class="content px-4">
#Body
</article>
</main>
</div>
#code {
protected override void OnInitialized()
=> this.LayoutService.MenuChanged += this.MenuChanged;
private void MenuChanged(object? sender, EventArgs e)
=> this.InvokeAsync(StateHasChanged);
public void Dispose()
=> this.LayoutService.MenuChanged -= this.MenuChanged;
}
And example page:
#page "/"
#inject LayoutService LayoutService
Page Content
#code {
protected override void OnInitialized()
{
this.LayoutService.ChangeMenu(typeof(Menu1));
}
Don't get too focused on the layout as a single entity that you have to use for every page in the whole site. You can have as many Layout components as you want, and you can nest them just like you would with any class and derived class.
https://blazor-university.com/layouts/nested-layouts/

Pagination links is not working properly in Laravel Livewire

I am using Laravel 9 and Livewire 2.x. Pagination links are not working correctly; it only changes one item. I tried to change the link on the address bar like /search?query=example&page=2, and that worked perfectly.
Component Class
use Livewire\Component;
use App\Models\Product;
use Livewire\WithPagination;
class Search extends Component
{
use WithPagination;
protected $paginationTheme = 'bootstrap';
public $query;
protected $queryString = ['query'];
public function render()
{
$products = $this->searchQuery($this->query);
return view('livewire.products.search', [
'products' => $products->paginate(6),
])->extends('layouts.buyer')->section('content');
}
public function mount()
{
}
public function searchQuery($input){
return Product::where('name','like','%' . $input . '%' );
}
}
View
<div class="container my-5">
#if($products->isNotEmpty())
<div class="row g-4">
#foreach($products as $product)
<div class="col-lg-3">
#livewire('products.buyer-product-item', ['product' => $product])
</div>
#endforeach
</div>
#else
<div class="alert alert-danger">Product Was Not Found !!!</div>
#endif
<nav class="mt-5" aria-label="Page navigation example">
{{ $products->links() }}
</nav>
</div>
Hey After viewing the console I found there is an error.
Cannot read property 'fingerprint' of null
After including the key for the nested component the issue was solved.
#livewire('products.buyer-product-item', ['product' => $product], key($product->id))

lit-element - How to call an event from a slot button?

element
import { LitElement, html } from 'lit-element';
class Component extends LitElement {
render () {
return html`
<slot name="activator">
<button #click="${this.openDialog}">Default Button</button>
</slot>
<custom-dialog></custom-dialog>
`;
}
openDialog () {
// code to open dialog
}
}
customElements.define('custom-dialog', Component);
index.html
<head>
<script type="module" src="src/custom-dialog.js"></script>
</head>
<body>
<custom-dialog>
<button slot="activator">Outside Button</button>
</custom-dialog>
</body>
Given the custom component above and my implementation on a simple html page. You'll notice that I'm using a slot button.
How do I call the openDialog() method using the slot button?
I checked docs for events but I found nothing relevant to this.
Thanks in advance.
You need a click event listener on the slot or some ancestor of the slot:
Try moving the #click binding to the slot element itself. click events bubble, so this will handle both the default slot content button and the slotted button from the light DOM. This might not work in ShadyDOM, so you may want to put the event listened on a wrapper element around the slot.
import { LitElement, html } from 'lit-element';
class Component extends LitElement {
render () {
return html`
<slot name="activator" #click="${this.openDialog}">
<button>Default Button</button>
</slot>
<custom-dialog></custom-dialog>
`;
}
openDialog () {
// code to open dialog
}
}
customElements.define('custom-dialog', Component);

ASP.NET core persisting values between Get and Post error validation

I'm new to web development so I don't know a good way on how to persist data between requests.
This is my site so far:
The elephant title is being fetched from an API on the GET request, according to the titleId query parameter. When I press login, model validations are being run, for example that email and password must have been entered. However, when error page is returned, elephant text is empty since that value was not persisted. What are the best approaches to persist that value so that is still visible when POST error is returned? Does it has to be included in the POST data? I don't want to request the API again.
Code behind:
public class IndexModel : PageModel
{
private string apiTitle;
public string ApiTitle { get { return apiTitle; } set { apiTitle = value; } }
// Bind form values
[BindProperty]
public User user { get; set; }
public Task<IActionResult> OnGetAsync(string titleId)
{
if (!string.IsNullOrEmpty(titleId))
{
ApiTitle = await GetTitleFromApiAsync(titleId);
}
return Page();
}
public async Task<IActionResult> OnPostLoginAsync()
{
if (!IsLoginFormValid())
{
// When this is returned, for example if no password was entered,
// elephant title goes missing since apiTitle is null?
return Page();
}
var user = await LoginEmailAsync();
return RedirectToPage("/Profile");
}
}
Html:
#page
#model IndexModel
#{
ViewData["Title"] = "Home page";
}
<link rel="stylesheet" href="css/index.css">
<script src='http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js'></script>
<div class="login-page">
<span id="label_api_title">#Model.ApiTitle</span>
<div class="form">
<form class="login-form" method="post" asp-page-handler="Login">
<input type="text" placeholder="Email" asp-for="User.Email"/>
<span asp-validation-for="User.Email" class="text-danger"></span>
<input type="password" placeholder="Password" asp-for="User.Password1" />
<span asp-validation-for="User.Password1" class="text-danger"></span>
<button>login</button>
<p class="message">Not registered? Create an account</p>
</form>
</div>
</div>
<script src="js/index.js"></script>
Yes. What you see is the expected behavior. Remember, Http is stateless. You are making 2 separate http calls, one for the GET and one for POST. The second call has no idea what the first call did ( or even there was first call at all!)
If you want to have a way to read the ApiTitle property value in the Post call and return that to the view, you need to persist it somewhere so that it is available between http calls. But in your case, all you need is to include that in the form post and have the framework bind it for you.
In your case, you can simply use a public property (Which is settable and gettable) for this. No need to keep a private variable. Decorate your property with BindProperty attribute so the model binder will bind the data on this property.
public class CreateModel : PageModel
{
[BindProperty]
public string ApiTitle { get; set; }
//Your existing code goes here
}
Now inside your form tag, have an input hidden element for the ApiTitle. This way, when the form is submitted, the value of ApiTitle property will be send in the request data.
<form class="login-form" method="post" asp-page-handler="Login">
<input type="hidden" asp-for="ApiTitle"/>
<input type="text" placeholder="Email" asp-for="User.Username"/>
<!--Your other existing form elements -->
<button>login</button>
</form>
Now in your OnPostLoginAsync method, you can read the ApiTitle value if needed. When you return the Page (when validation fails), the UI will display the ApiTitle property value in your span element.
public async Task<IActionResult> OnPostLoginAsync()
{
var title = this.ApiTitle; // If you want to read this value
if (!ModelState.IsValid)
{
return Page();
}
return RedirectToPage("/Profile");
}
It could be that your not sending it inside your form in the razor and its only in the get request and not in the post request:
public Task<IActionResult> OnGetAsync(string titleId)//<----its here
{
if (!string.IsNullOrEmpty(titleId))
{
ApiTitle = await GetTitleFromApiAsync(titleId);
}
return Page();
}
//-->need to pass title id in post
public async Task<IActionResult> OnPostLoginAsync(string titleId)
{
if (!string.IsNullOrEmpty(titleId))
{
ApiTitle = await GetTitleFromApiAsync(titleId);
}
if (!IsLoginFormValid())
{
// When this is returned, for example if no password was entered,
// elephant title goes missing since apiTitle is null?
return Page();
}
var user = await LoginEmailAsync();
return RedirectToPage("/Profile");
}
and in your razor add the span in the form:
<span id="label_api_title">#Model.ApiTitle</span>
<div class="form">
<form class="login-form" method="post" asp-page-handler="Login">
<span id="label_api_title">#Model.ApiTitle</span>/*<--------here*/
<input type="text" placeholder="Email" asp-for="User.Email"/>
<span asp-validation-for="User.Email" class="text-danger"></span>
<input type="password" placeholder="Password" asp-for="User.Password1"/>
<span asp-validation-for="User.Password1" class="text-danger"></span>
<button>login</button>
<p class="message">Not registered? Create an account</p>
</form>

Panel created dynamically is shown on XHTML but missing on getViewRoot

Panel is created and added to xhtml right but when I get it using findComponent("-formBotones-wizardEventContainer").getChildren() the result is null because its container got 1 children (id=tempTextIdLayer) when really html source show several children.
I've tried a solution with visitTree() but vars target and panel are the same, wizarEventContainer, and the got only 1 children (input hidden)
HtmlPanelGroup panel=(HtmlPanelGroup)FacesContextWrapper.getCurrentInstance().getViewRoot().findComponent("formBotones-wizardEventContainer");
panel.visitTree(VisitContext.createVisitContext(FacesContext.getCurrentInstance()),new VisitCallback() {
#Override
public VisitResult visit(VisitContext context, UIComponent target) {
if (target instanceof HtmlPanelGroup) {
HtmlPanelGroup layer = (HtmlPanelGroup) target;
System.out.println("id: " + layer.getId()); //wizardEventContainer // Collect them in an arraylist orso.
}
return VisitResult.ACCEPT;
}
});
Main xhtml code
<h:form id="formBotones" prependId="true">
<h:panelGroup layout="block" id="wizardEventContainer" styleClass="wizardEventContainer">
<h:inputHidden id="tempTextIdLayer" value="#{eventProvider.tempTextIdLayer}" />
</h:panelGroup>
</h:form>
HTML when panel is added
<form id="formBotones">
<input type="hidden" value="formBotones" name="formBotones">
<div class="wizardEventContainer" id="formBotones-wizardEventContainer">
<div data-widget="widget_formBotones_text_11_0" id="formBotones-text_11_0">
<div class="ui-panel-content ui-widget-content" id="formBotones-text_11_0_content">
<span id="formBotones-editor_text_11_0">Dynamic text</span>
</div>
<div class="ui-resizable-handle ui-resizable-e" style="z-index: 90;"></div>
<div class="ui-resizable-handle ui-resizable-s" style="z-index: 90;"></div>
<div class="ui-resizable-handle ui-resizable-se ui-icon ui-icon-gripsmall-diagonal-se" style="z-index: 90;"></div>
</div>
<input type="hidden" value="text_11_0" name="formBotones-tempTextIdLayer" id="formBotones-tempTextIdLayer">
</div>
<input type="hidden" value="e5s4" id="javax.faces.ViewState" name="javax.faces.ViewState">
</form>
Create layers code
Here, it's a method which is setted to button's actionListener. It create a resizable and draggable panel with a span where I'll write dynamic text through javaScript.
public void createTextLayer() throws IOException{
logger.entry("EventProvider.createTextLayer()");
Application app=FacesContextWrapper.getCurrentInstance().getApplication();
UIComponent parent=FacesContextWrapper.getCurrentInstance().getViewRoot().findComponent("formBotones-wizardEventContainer");
CommandButton textBtn=(CommandButton)FacesContextWrapper.getCurrentInstance().getViewRoot().findComponent("formControl-textLayerBtn");
String finalId=null;
if(tempObjs!=null && tempObjs.size()>0 && tempObjs.containsValue("text")){
finalId="text_" + space.getIdSpace() + "_" + createFinalIndex("text");
}
else{
finalId="text_" + space.getIdSpace() + "_" + spaceBo.getDao().countSectionPages(space.getIdSpace(),4);
}
if(parent!=null){
HtmlPanelGroup panel=(HtmlPanelGroup)app.createComponent(HtmlPanelGroup.COMPONENT_TYPE);
panel.setId(finalId);
panel.setStyleClass(panel.getId());
panel.setStyle("display:inline-block;min-width:100px;min-height:100px;background:red;overflow:hidden;");
setTempTextIdLayer(finalId);
HtmlOutputText text=(HtmlOutputText) app.createComponent(HtmlOutputText.COMPONENT_TYPE);
text.setId("editor_" + panel.getId());
panel.getChildren().add(text);
Resizable resizable=(Resizable)app.createComponent(Resizable.COMPONENT_TYPE);
resizable.setFor(panel.getId());
resizable.setMaxWidth(new Integer(800));
resizable.setMaxHeight(new Integer(600));
resizable.setMinWidth(new Integer(50));
resizable.setMinHeight(new Integer(50));
resizable.setContainment(false);
resizable.setFor(panel.getId());
panel.getChildren().add(resizable);
Draggable drag=(Draggable)app.createComponent(Draggable.COMPONENT_TYPE);
drag.setFor(panel.getId());
drag.setOpacity(new Double(0.6));
drag.setContainment(new String("parent"));
drag.setSnap(true);
drag.setSnapMode(new String("outer"));
drag.setSnapTolerance(new Integer(5));
drag.setFor(panel.getId());
panel.getChildren().add(drag);
tempObjs.put(panel,"text");
}
updateViewRoot();
//Disabling text button
textBtn.setDisabled(true);
}
Add layer to container code
Here I get the container, wizardEventContainer, and I add every panel which are saved on tempObjs var.
public void updateViewRoot(){
logger.entry("EventProvider.updateViewRoot()");
UIComponent parent=FacesContextWrapper.getCurrentInstance().getViewRoot().findComponent("formBotones-wizardEventContainer");
if(parent!=null){
for (Entry<UIComponent, String> entry : tempObjs.entrySet()) {
parent.getChildren().add(entry.getKey());
}
}
}
As getViewRoot() only return elements which are written on xhtml file I've decided to save dynamic components in a map and when I place this dragabble component in the right site the I pass css style and other properties through managed bean properties, so every time a new panel is added on xhtml I create a new dynamic component with new properties setted and I replace for old dynamic component saved in the map.
It's a no elegant way but works fine at the moment for me.
public void saveTextLayer(){
logger.entry("EventProvider.saveTextLayer()");
List<UIComponent> list=null;
if(tempObjs.containsValue("text"))
list=getKeysByValue(tempObjs,"text");
for (UIComponent c:list){
if (c instanceof HtmlPanelGroup){
HtmlPanelGroup panel=(HtmlPanelGroup)c;
if(panel.getId().equals(getTempTextIdLayer())){
panel.setStyle(getTempCSS());
if(panel.getChildren().get(0) instanceof HtmlOutputText){
((HtmlOutputText)panel.getChildren().get(0)).setValue(getTempHTML());
}
//Adding delete layer
Application app=FacesContextWrapper.getCurrentInstance().getApplication();
HtmlPanelGroup deleteLayer=(HtmlPanelGroup)app.createComponent(HtmlPanelGroup.COMPONENT_TYPE);
deleteLayer.setId("delete_" + panel.getId());
deleteLayer.setStyle("display:inline-block;width:5px;height:5px;background:white;");
deleteLayer.setOnclick("deleteLayer($(this));");
panel.getChildren().add(0,deleteLayer);
//Replacing UIComponent
tempObjs.remove(c);
tempObjs.put(panel, "text");
updateViewRoot();
//Deleting temp vars
setTempCSS("");
setTempTextIdLayer("");
setTempHTML("");
}
}
}

Resources