This is a solution to the Tip calculator app challenge on Frontend Mentor. Frontend Mentor challenges help you improve your coding skills by building realistic projects.
Users should be able to:
- View the optimal layout for the app depending on their device's screen size
- See hover states for all interactive elements on the page
- Calculate the correct tip and total cost of the bill per person
- Solution URL: Github
- Live Site URL: Netlify app
- Semantic HTML5 markup
- Vue 3 (Composition API)
- Typescript
- Tailwind CSS
I used this project to learn Typescript, which I had been meaning to learn for a while. As someone who has some experience with typed languages, it was interesting to bring the safety it offers to a web project. This app isn't complex enough to need this kind of safety, but I was surprised to see how much more readable it made the code.
Take this as an example:
const splitTitle = (word: string, splitAt: number[]) => {
const first = word.slice(splitAt[0], splitAt[1])
const last = word.slice(splitAt[1])
return `${first} <br /> ${last}`
}
splitTitle('Splitter', [0, 4])
// returns:
//spli
//tter
This function splits a word into two parts based on the specified place, and prints it out in two lines. Using Typescript we can declare the types of the parameters, making the function easier to understand.
Types can be specified through inference, meaning that this variable will have a type of Array
without having to be explicitly declared: const tipAmounts = reactive([5, 10, 15, 25, 50])
.
When using Vue with Typescript, I had difficulty working with otherwise simple functionality at first. One has to be more explicit when declaring props, since types obviously matter. A variable of type number
can't be used as an input's name, since it is a number. To use it properly, string interpolation can be used to pass the value as a string:
<input
type="number"
:name="`${amount}`"
:id="`amount-${amount}` />
Another thing I learned is that it is better to use strings as the first part of an id when working with inputs.
The object syntax is also useful for declaring props with types and default values, as opposed to the simpler array syntax I had been using in the past:
defineProps({
amount: {
type: Number,
default: 0
}
})
Despite the simplicity of this project, I found many solutions which had accessibility issues due to a lack of semantic markup and missing label and input attributes. It should be noted that the design itself isn't accessible to users with vision impairment, as a quick look at the accessibility tab of the dev tools will show that certain text leafs don't have sufficient contrast.
While creating the NumberInput
component I made use of semantic markup and learned some best practices along the way to make it reusable and accessible. The input can access a name and an id based on props to bind it with the label, and an optional placeholder can also be specified.
It was interesting to create something based on an existing design again while making sure to keep it responsive and as close as possible to the original design. I used the colours provided by the design file to extend the Tailwind theme and use them across the app. Flexbox and Grid were also used to layout the contents of the app.
Now that I have gotten down the basics of Typescript, I would like to focus on a more complex project where union types and interfaces could be used, among more advanced concepts.
- Typescript Handbook - For learning the basics.
- Typescript with Composition API - For learning how to use Typescript with Vue's Composition API.
- Customizing Tailwind - Helped me learn some new things about Tailwind which made my code cleaner.
- Github - George Daris
- Frontend Mentor - @GeorgeDaris
I would like to thank the folks over at the VueLand Discord server, who were kind enough to help me out with questions regarding Typescript with Vue.