Stock Toolkit: Conclusion
My toy stock toolkit application is “feature complete” for now. I’m very happy with both the quality of the tooling and the ease of using it. Svelte combines well with Tailwind and lets me get things done on the front end more easily than I imagined. This post summarizes what I made along with what I’ve learned.
My code is available in this repository. Bear in mind it is the very opposite of “battle-tested” in the unlikely event you do anything other than just look at it.
Background
My web development skills have tilted toward heavy, server-side things over the years. I wanted to update my tools and my skills over the full stack, but especially on the front end, where I was feeling closer to 1997 than 2020. So I took inspiration from this tutorial and did the backend in FastAPI with a different database layer and did the front end in Svelte with Tailwind CSS.
For more details, look at the previous posts:
- Introduction to my FastAPI and Svelte setup
- Detailed setup
- PyCharm setup for front-end work
- My detour into smelte (not part of the end product)
- Getting vanilla Tailwind integrated with svelte build
I’ve attempted to update them with new things I’ve learned, as applicable.
The Product
This application lets the user make a list of stocks, by ticker symbol. It fetches details about these stocks from Yahoo finance. It lets the user filter based on these details, add new symbols, and delete symbols. It isn’t a particularly exciting set of functionality, but it covers a surface area that lines up fairly neatly with the things I usually want to build. This screenshot should give the general idea:
I used svelte without leaning heavily on any UI component library. While I spent quite a bit of time exploring Svelte Material UI and smelte and liked both a great deal, I ultimately felt like I needed to avoid both for now. A big part of that is needing to understand svelte more deeply. For now, those both hide too much magic. smui uses sass, which I don’t understand very well. smelte uses typescript, which I also don’t understand very well. I’m interested in both (especially typescript) but felt like I was collapsing under an attempt to learn too many new things at once while hiding parts of svelte I want to understand before I use other people’s components under a bit too much magic. I was also having a hard time making smelte play well with my own usage of Tailwind. I do want to go back and look at both again once I’ve got some of my own components under my belt.
A Quick Tour
Full source is available here. Snippets below are just highlights.
Application Shell
The scripts in App.svelte
are high on my list of things to golf down if I continue to do anything else with this. All of the API interaction is there and is a strong candidate for moving out to its own module. And there’s some event handling there that could probably be done just by reacting to changes to a data store. Leaving those aside, though, the app component is very simple:
<style>
:global(.max-w-app) {
@apply max-w-6xl;
}
</style>
<div>
<AppHeader
actions={actionBarItems}
on:fetchData={onFetchData}
on:addStock={onAddSymbols}
/>
<div class="max-w-app mx-auto py-6 sm:px-6 lg:px-8">
{#if $loading && stocks.length <= 0}
<div class="flex justify-center">
<Spinner size="200" color="#ff5a1f"/>
</div>
{:else}
{#if stocks && stocks.length > 0}
<StockToolbar {selectedStocks} on:deleteStocks={doDeleteStocks} on:applyFilter={onFetchData}/>
<StockTable tableData={stocks} bind:selected={selectedStocks}/>
{:else}
<div class="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4" role="alert">
<p class="font-bold">Warning</p>
<p>No stock data was found.</p>
{#if !last_filter_empty}
<p class="text-xl text-red-600">A filter is currently active.</p>
{/if}
<div class="flex flex-row-reverse">
<Button classes="ml-2 bg-green-600" name="Add a New Stock" on:click={onAddSymbols} />
{#if !last_filter_empty}
<Button classes="ml-2 bg-orange-600" name="Remove Filter" on:click={() => {
filter.set({...$filter,
forward_pe: "",
dividend_yield: "",
ma50: false,
ma200: false,
})
onFetchData();
}}
/>
{/if}
</div>
</div>
{/if}
{/if}
</div>
<Modal>
<DialogController bind:this={dialogController} on:doAddSymbols={doAddSymbols}/>
</Modal>
</div>
By bulk, the largest part of it is some UI niceness for when no stocks are returned by the API. It also shows the two library components I did wind up using: svelte-spinner and svelte-simple-modal. I used the spinner because making one wasn’t interesting to me and I like it better than just showing the word “Loading…”. I used svelte-simple-modal because I realized I was recreating it less robustly as I was about a third of the way through making my own modal dialog class. The interesting thing about it here is that the component which shows the dialog needs to be a child of a Modal
to get at its context.
The header and the stock toolbar communicate with the app shell using both events and stores.
Using Stores
I used stores for two purposes. The first, to set a “loading” flag so that various components can decline to trigger API requests while the stock table is changing probably would’ve worked with simpler bindings. I mainly did it to learn how to use the writable store. The filter is shared between the app and the toolbar this way, too. I originally passed that as part of an event, but I tried it this way so that I could learn how to use something a little more complicated than a bool or an int in a store. I’m glad I did that, because there was an interesting and non-obvious (to me) wrinkle.
My filter store:
export const filter = writable(
{
forward_pe: "",
dividend_yield: "",
ma50: false,
ma200: false,
is_empty: function() {
return (this.forward_pe.length === 0 &&
this.dividend_yield.length === 0 &&
!this.ma50 && !this.ma200
);
}
}
)
has a function to query for whether a filter is empty. I first learned that, a little contrary to my intuition, something like $filter.forward_pe = "42"
does not update the store. And using a setter with a dictionary like this:
filter.set({"forward_pe": "42", "dividend_yield": "0", "ma50":true, "ma200":false})
gets rid of the is_empty() function. So this was the simplest way I could find to update the filter:
function save_filter() {
filter.set({...$filter,
forward_pe: pe_filter, dividend_yield: dividend_filter, ma50: ma50, ma200: ma200
});
}
Message Dispatching
I really like the way svelte’s message dispatching works, but one thing that bit me a little was the inability to bubble up custom messages whose names may be calculated at runtime. The usual way of adding on:event
without assignment to a component doesn’t work because there seems to be no way to have event
be the result of evaluating a variable.
I wanted to use custom events to make buttons pass messages to a controller. e.g.:
{#each toolbarItems as tbItem}
<Button action={tbItem.action} icon={tbItem.icon} on:{tbItem.action}>
{/each}
That would seem to be a broadly useful pattern, but I couldn’t see a way to do that directly. It feels like it might be worth asking around about.
Stock Table
I really like my stock table component with selectable rows. I think it turned out well, and I spent less time writing it than I did trying to figure out how to alter the one from smelte to have selectable rows. I still want to go back and look at that again, though, because smelte’s is overall a nicer table. I am especially happy with the behavior of clicking a row to select it. While I like how it turned out, it smells like I might’ve missed an opportunity to use some of the built in reactivity offered by the framework. I had a click on a row trigger a javascript function:
<tr class="bg-white lg:hover:bg-gray-100 flex lg:table-row flex-row border-b border-l border-r lg:flex-row flex-wrap lg:flex-no-wrap mb-10 lg:mb-0"
on:click={clickSelected(record)}>
The easiest way I found to keep everything synchronized with the binding was to have clickSelected send a click to the checkbox:
// this is here because I couldn't figure out how to get everything to react as if I'd clicked the checkbox
// by merely toggling its checked state. there may be a better way to do it; this way requires a click handler
// on the checkbox to stop propagation in order to allow both direct clicks and clicks on the row.
function clickSelected(record) {
if(!selectable) return;
let checkbox = document.getElementById('select-' + record.id);
checkbox.click();
}
And then preventing that from re-toggling the value:
<input type="checkbox" class="form-checkbox text-green-600" id="{'select-' + record.id}"
bind:group={selected} value={record} on:click={(e) => {e.stopPropagation()}}/>
Also, the relative ease of getting the column header’s checkbox to reflect the actual selection was very pleasing:
// make the state of the select all checkbox reflect the actual selection
$: if(selectable && typeof(selectAll) !== 'undefined'){
let nRecords = tableData.length;
if (selected.length > 0) {
if (selected.length < nRecords) {
selectAll.indeterminate = true;
} else {
selectAll.indeterminate = false;
selectAll.checked = selected.length === nRecords;
}
} else {
selectAll.indeterminate = false;
selectAll.checked = false;
}
}
Reactive CSS Classes
In more than one place, I used a pattern like this:
<div class:opacity-50={disabled}>
to change the appearance of UI in response to application state changes. While that worked well in development builds, in some release builds the tailwind CSS purge step was removing opacity-50 as unused. The reliable way around this that I found was to write something like this in the style block for the component:
<style>
.st-btn.disabled {
@apply opacity-25 elevation-0;
}
</style>
<div class="st-btn" class:disabled={disabled}>
That seemed to reliably prevent the conditionally applied classes from getting purged.
I found this issue which looks like it might have some better work-arounds.
Final notes (for now)
I’m really happy with the level of richness I got out of this front end for a relatively small amount of code. I wrote less, understand it better, and got a better result than I’m used to.
There are a number of things I’d change if I were going to touch this much more:
- refactor api communications into a module
- better feedback on deletion
- add a websocket channel for the server to tell the client when stock data has changed
- sorting
- authentication
I might pick this back up if I decide I want to learn how to push an update over a websocket or experiment with authentication; this seems like a good platform to do it on.
What I really feel the need to do next, though, is get my testing and deployment story down. I know this is relatively easy to host. Learning that might include getting sapper set up for server side rendering, though that’s a lower priority than getting a really good understanding of the path from git push to production availability.
If anyone has read this going-on-book-length set of posts or just looked at my code, and wants to offer feedback, questions or advice, I posted it to /r/sveltejs and will read/reply to comments there.