Trello: Convertendo VUE para Blazor Web Assembly

Dicas para converter uma aplicação clone do Trello de VUE para Blazor no modelo Web Assembly.

Trello: Convertendo VUE para Blazor 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.

DESIGN PATTERNS Programação no Mundo Real

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>&nbsp;
                <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.
Sobre o Autor:
Trabalha como arquiteto de soluções e desenvolvedor, tem mais de 18 anos de experiência em desenvolvimento de software em diversas plataformas sendo mais de 16 anos somente para o mercado de seguros.