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!
Table of Contents
# 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. |


