Dicas para converter uma aplicação clone do Trello de VUE para Blazor no modelo Web Assembly.
Aplicação clone do Trello?
Sim, trata-se de uma aplicação web desenvolvida pelo Lucas Juliano baseada no visual da ferramenta Trello e como objetivo, demonstrar conceitos de componentização no VUE.
Então, esse artigo é mais uma parceria realizada entre nós dois, ou seja, o Lucas desenvolveu todo frontend em VUE e eu o converti para o Blazor.
Vamos ver como ficou essa conversão!
# O Trello e o Trellu
Aqui na empresa nós usamos o Trello para controlar as solicitações/chamados de clientes tanto para manutenção, novos projetos ou simplesmente suporte ao usuário.
Trata-se de um dashboard online estilo Kanban que possibilita em tempo real a criação de cartões com objetivo de organizar e controlar demandas de clientes.
O uso do Trello é gratuito, dá uma olhada aqui depois.
Já o Trellu (com “u”), como comentei, foi desenvolvido pelo Lucas apenas para demonstrar as funcionalidades e componentização nas tecnologias VUE e Blazor Web Assembly.
Dá uma olhada como ficou a versão Blazor Web Assembly:
Em seu blog, Lucas explica todas as questões do VUE. Nesse artigo, o objetivo é focar apenas nas funcionalidades da versão Blazor.
É óbvio, que nem ele e nem eu criamos todas as funcionalidades do Trello, isso seria um absurdo e levaria muito tempo.
# Blazor Web Assembly
Se você não viu nada sobre o Blazor, sugiro dar uma olhada em outros artigos que escrevi, por exemplo: Blazor: Muito mais que um WebForms de Luxo e Blazor Web Assembly: Doces ou Travessuras?.
Apesar do Blazor Web Assembly estar em versão Preview (até a conclusão desse artigo), mesmo assim dá para demonstrar algumas coisas bem interessantes.
Blazor é considerado o SPA (Single Page Application) da Microsoft que poderia bater de frente e substituir Angular, Vue, React e outros frameworks javascript.
Quando desenvolvemos uma aplicação Blazor, nós criamos uma aplicação web ASP.NET CORE 3.1.
Vamos ver o código!
# Código fonte
Eu tentei fazer o mais parecido possível com o que o Lucas fez no projeto VUE dele usando os mesmos nomes de componentes, propriedades e eventos.
Na solution do Visual Studio, você encontrará três projetos: Uma projeto Class Library do meu Framework com funcionalidades genéricas; outro projeto Class Library para guardar as regras de negócio; e por fim, a aplicação Blazor Web Assembly, que contém todo o frontend.
No arquivo wwwroot/index.html adicionei referências a font family Google e ícones do fontawesome.
Já no arquivo Shared/MainLayout.cshtml coloquei todo o CSS que o Lucas customizou em seu artigo.
Interessante ressaltar que o comando CSS “!important” não funciona no Blazor, assim tive que remover todas as ocorrências do CSS. Tentei pesquisar mais a respeito pra tentar resolver essa questão, mas não encontrei nada que resolvesse.
Abaixo segue a lista de todos os componentes Blazor convertidos a partir do VUE.
Eu sei que é difícil entender o código fonte dessa maneira, assim sugiro você baixá-lo diretamente do meu github.
Pages/Index.razor (para VUE clique aqui):
@page "/" @inject NavigationManager NavigationManager @code{ protected override void OnAfterRender(bool firstRender) { NavigationManager.NavigateTo("board/1/my"); } }
Components/Board.razor (para VUE clique aqui):
@using FSL.VueTo.Core.Models @page "/board/{id}/{title}" <div class="board"> <Header> <Navbar Title="@Title" /> </Header> <div class="board"> <ViewList Lists="lists" /> </div> </div> @code{ List<ListItem> lists; [Parameter] public string Title { get; set; } [Parameter] public string Id { get; set; } protected override void OnInitialized() { base.OnInitialized(); var listId = Guid.NewGuid().ToString(); Title = "Lucas Juliano Company"; lists = new List<ListItem>(); lists.Add(new ListItem { Title = "List 1", Id = listId, Items = new List<Item>() { new Item { Description = $"Item from list '{listId}'", Id = Guid.NewGuid().ToString(), Date = "", Title = "New Item" } } }); } }
Shared/Navbar.razor (para VUE clique aqui):
<div> <div class="navbar-menu"> <div class="navbar-start" style="margin-top:5px"> <label for="Lucas Juliano" class="lbl-titulo-board">@Title</label> <ButtonIcon Style="margin-left:5px" Icon="far fa-star" /> <button class="button btn-boards is-small"> <span>Quadros</span> <span class="tag is-info is-rounded btn-boards-type">Free</span> </button> <ButtonIcon Label="Particular" Icon="fas fa-lock" /> <figure class="image is-32x32" style="margin-top:5px;margin-right:5px"> <img class="is-rounded" src="https://trello-avatars.s3.amazonaws.com/56d80c98213de6cf5319b5ce3037880d/30.png" /> </figure> <ButtonIcon Label="Convidar" /> </div> <div class="navbar-end"> <ButtonIcon Label="Clean Board" Icon="far fa-trash-alt" OnClickAsync="OnClickAsync" /> <ButtonIcon Label="Butler (3 Tips)" Icon="fab fa-trello" /> <ButtonIcon Label="Show Menu" Icon="fab fa-trello" /> </div> </div> </div> @code{ [Parameter] public string Title { get; set; } [Parameter] public EventCallback<string> OnClickAsync { get; set; } }
Shared/Header.razor (para VUE clique aqui):
<div> <div class="navbar-menu" style="background:#0067A3"> <div class="navbar-start"> <!-- navbar items --> <ButtonIcon Icon="fas fa-home" Style="margin-left:5px" OnClickAsync="OnClickAsync" /> <ButtonIcon Icon="fab fa-trello" Label="Quadros" /> <div class="field" style="margin:5px;margin-left:1px;"> <p class="control has-icons-right"> <input class="input is-small" type="text" style="background:#4D95BE;color:#fff;border-color:#4D95BE" /> <span class="icon is-small is-right"> <i class="fas fa-search"></i> </span> </p> </div> </div> <div class="navbar-center"> <img src="img/logo-trello.png" /> </div> <div class="navbar-end"> <ButtonIcon Icon="fas fa-plus" /> <ButtonIcon Icon="fas fa-exclamation-circle" /> <ButtonIcon Icon="far fa-bell" /> <figure class="image is-32x32" style="margin-top:5px;margin-right:5px"> <img class="is-rounded" src="https://trello-avatars.s3.amazonaws.com/56d80c98213de6cf5319b5ce3037880d/30.png" /> </figure> </div> </div> <CascadingValue Value=this> @ChildContent </CascadingValue> </div> @code{ [Parameter] public string Title { get; set; } [Parameter] public RenderFragment ChildContent { get; set; } [Parameter] public EventCallback<string> OnClickAsync { get; set; } }
Shared/ButtonIcon.razor (para VUE clique aqui):
@using FSL.Framework.Core.Extensions <div style="@(Style)"> <button class="button is-small btn-container" @onclick="OnClickingAsync"> @if (!Icon.IsNullOrEmpty()) { <span class="icon @Size"> <i class="@Icon"></i> </span> } @if (!Label.IsNullOrEmpty()) { <span v-if="label">@Label</span> } </button> </div> @code{ [Parameter] public string Label { get; set; } [Parameter] public string Color { get; set; } [Parameter] public string Size { get; set; } [Parameter] public string Icon { get; set; } [Parameter] public string Style { get; set; } [Parameter] public EventCallback<string> OnClickAsync { get; set; } protected async Task OnClickingAsync( EventArgs e) { if (OnClickAsync.IsNotNull()) { await OnClickAsync.InvokeAsync(Label); } } protected override void OnInitialized() { base.OnInitialized(); Size = Size ?? "is-small"; Label = Label ?? ""; Color = Color ?? ""; Style = Style ?? ""; Icon = Icon ?? ""; } }
Components/ViewList.razor (para VUE clique aqui):
@using FSL.VueTo.Core.Models @using FSL.Framework.Core.Extensions <div class="lists-container" id="style-2"> @foreach (var list in Lists) { <section class="list-container" ref="@list.Id" data-id="@list.Id"> <div class="list-header">@list.Title</div> <div> @foreach (var item in list.Items) { <Card Item="item" /> } </div> <div class="footer-container-list"> <button class="button is-light is-fullwidth" @onclick="a => OnAddCard(list.Id)"> <span class="icon"> <i class="fas fa-plus"></i> </span> <span>Add New Card</span> </button> </div> </section> } <div class="add-list-container"> <AddList Placeholder="Add New List" OnClickAsync="OnClickingAsync" /> </div> </div> @code{ [Parameter] public string Title { get; set; } [Parameter] public string Placeholder { get; set; } [Parameter] public List<ListItem> Lists { get; set; } protected override void OnInitialized() { base.OnInitialized(); Lists = Lists ?? new List<ListItem>(); Title = Title ?? ""; Placeholder = Placeholder ?? ""; } [Parameter] public EventCallback<string> OnClickAsync { get; set; } protected async Task OnClickingAsync( string title) { Lists.Add(new ListItem { Title = title, Id = Guid.NewGuid().ToString() }); if (OnClickAsync.IsNotNull()) { await OnClickAsync.InvokeAsync(Title); } } protected void OnAddCard( string listId) { var listItem = Lists.FirstOrDefault(x => x.Id == listId); listItem.Items.Add(new Item { Id = Guid.NewGuid().ToString(), Title = $"New Item", Description = $"Item from list '{listId}'" }); } }
Components/AddList.razor (para VUE clique aqui):
@using FSL.Framework.Core.Extensions <div class="ui-item-entry field has-addons"> <div class="control is-expanded"> <input class="input" @bind="input" placeholder="@Placeholder" @onkeyup="OnKeyUpAsync" /> </div> @if (!Icon.IsNullOrEmpty()) { <div class="control"> <button type="submit" class="button is-primary" @onclick="OnClickingAsync" disabled="@input.IsNullOrEmpty()"> <span class="icon is-small"> <i class="fas fa-@Icon"></i> </span> </button> </div> } </div> @code{ string key = ""; string input = ""; [Parameter] public string ListId { get; set; } [Parameter] public string Placeholder { get; set; } [Parameter] public string Icon { get; set; } protected override void OnInitialized() { base.OnInitialized(); Icon = Icon ?? "angle-right"; } [Parameter] public EventCallback<string> OnClickAsync { get; set; } protected async Task OnClickingAsync( EventArgs e) { if (OnClickAsync.IsNotNull()) { await OnClickAsync.InvokeAsync(input); } input = ""; } protected async Task OnKeyUpAsync( KeyboardEventArgs e) { if (e.Key != "Enter") { return; } await OnClickingAsync(e); } }
Components/Card.razor (para VUE clique aqui):
@using FSL.VueTo.Core.Models @using FSL.Framework.Core.Extensions <div class="card @Classes" data-id="@Item.Id"> <div class="icons"> <span v-if="isDue" class="icon icon-due has-text-warning" title="@($"Item is due on {Item.Date}")"> <i class="fas fa-star"></i> </span> <span v-else-if="timestamp" class="icon icon-date" title="@($"Item is due on {Item.Date}")"> <i class="far fa-bell"></i> </span> <span class="icon icon-edit" @onclick="a => _dialog = true"> <i class="fas fa-edit"></i> </span> </div> <div class="list-drag-handle"> <p class="item-title">@Item.Title</p> @if (!Item.Description.IsNullOrEmpty()) { <p class="item-description">@Item.Description</p> } </div> <Dialog Title="@Item.Title" @bind-Toggle="@_dialog"> <div> <small>Title : @Item.Title</small> <p> Description : @Item.Description </p> </div> </Dialog> </div> @code{ protected override void OnInitialized() { _dialog = false; } bool _dialog = false; [Parameter] public string Classes { get; set; } [Parameter] public Item Item { get; set; } }
Shared/Dialog.razor (para VUE clique aqui):
<div> <div class="modal fade @Classes()"> <div class="modal-background"></div> <div class="modal-card"> <header class="modal-card-head modal-body"> <p class="modal-card-title"> @Title </p> <button class="delete" aria-label="close" @onclick="OnChangeAsync"></button> </header> <section class="modal-card-body modal-body"> <CascadingValue Value=this> @ChildContent </CascadingValue> </section> </div> </div> </div> @code{ protected override void OnAfterRender(bool firstRender) { _contador++; base.OnAfterRender(firstRender); } private int _contador; private string Classes() { return Toggle ? "is-active" : ""; } [Parameter] public RenderFragment ChildContent { get; set; } [Parameter] public string Title { get; set; } [Parameter] public bool Toggle { get; set; } [Parameter] public EventCallback<bool> ToggleChanged { get; set; } protected async Task OnChangeAsync() { Toggle = false; await ToggleChanged.InvokeAsync(Toggle); } }
# Considerações
Não tenho vergonha de dizer, coloquei todo o código fonte acima para ser indexado nos mecanismos de buscas.
Toda conversão entre tecnologias é problemática, sempre tem alguma coisa que não funciona, vide a questão “!import” no CSS.
Especificamente nessa de VUE X Blazor foi mais simples por ambos usarem os mesmos conceitos de SPA e MVVM.
Eu sinceramente tentei realizar as funcionalidade de drag & drop que o Lucas usou no seu código fonte. Acredito que isso tenha que ficar para um artigo específico por conter N conceitos e regras que deixariam esse aqui muito complexo.
Esperto que tenha gostado.
Obrigado.
Obs.: artigo publicado e não revisado.
Artigos sobre Blazor e ASP.NET CORE:
Blazor Web Assembly: Doces ou Travessuras?
Blazor Server 3: Consultas Dinâmicas no SQL Server
Crie seu Framework em ASP.NET CORE 3 e Blazor
Cookies: Identity no Blazor e ASP.NET CORE 3.0
Blazor: Muito mais que um WebForms de Luxo
Benchmark: ASP.NET 4.8 vs ASP.NET CORE 3.0
AppSettings: 6 Formas de Ler o Config no ASP.NET CORE 3.0
JWT: Customizando o Identity no ASP.NET CORE 3.0
Crie um Gerenciador de Arquivos do Zero em .NET Core e VueJS
IIS: Como Hospedar Aplicação .NET Core em 10 Passos
.NET Core para Desenvolvedores .NET
Blazor: O Começo do Fim do Javascript?
Faça download completo do código fonte no github. |