vue serve App.vue
As usual, Derrick taught me this. Vue is a UI framework, which provides foundation for software developers to build web programs. While this is the general definition of a framework, I'm not too sure how a UI framework is distinct from this definition, but I guess I'll learn as I go.
how is this diff from gcc vue serve app.vue preprocesses ???
gcc is compiler run the outputed binary filename.o
we use vue (UI framework)
- Generate a
package.json
to handle my 3rd party packages, where node and yarn understand package.json, where the list of dependencies and build scripts are documented.
To ask Vue to generate and installs the list of dependencies for us, in the root project directory, and provides a sample project
vue create boba-bill
Since we are using Vue to build our project, we want a couple of the default files generated by the vue create
command, including:
package.json
main.js
, that imports the Vue library and uses the Vue object to open and run our project, placed inside the "src" folder- config files like
babel.config.js
and vue.config.js
which sets the base url for production builds
We copied these files over and deleted everything else.
"Child components are like functions, and props are like arguments. Can use props to pass stuff"
In the early stages of my project, I was confused about everything that was going on and I couldn't describe my questions without really circumstantial questions regarding my code with screenshots. Derrick articulated my question best:
The checkbox is in Friend component but you want to have a list of what's checked in the FriendList parent component?
The reason why my code didn't work was because props are for passing data only from parent to child, so if you want to pass it the other way, in Vue, you emit events.
Derrick explains colloquially that
"something" can emit events and another part of your code can listen for those events and run stuff based on those events. So parent waits for x to happen, then when you, for example, click a child, it yells "I got clicked!" and parent hears it and runs some code
I'm super proud of when I finally learned how to allow the parent component to listen for a response from the child component!
In this super clear example of how to emit child events, one thing I was a little confused about was which code snippets belonged in the parent and child components. So first, I'd like to clarify that code snippets 1, 2, and 4 are in the parent components. Code snippets 3 and 5, the ones relating to the <button> Enlarge text </button>
and $emit
are in Child.
To summarize what the example in the Vue docs illustrates, you need to do 4 things to enable the parent component to listen for child events:
- a data property in the Vue component declaration to store the information you're listening for from the Child component (this is
postFontSize
from code snippet 1 from the Vue docs example)- located in parent component, written in [java]
<script>
- located in parent component, written in [java]
- specify using the data property you declared, where this information will be presented in the template (code snippet 2)
- Use the
v-on
directive while calling the child to listen for an event in child (in this case, when the button is pressed), and run some Javascript (which is to incrementpostFontSize
) when the child is triggered. (an example of this is in code snippet 4)- both this and #2 are in the parent component, in the
<template>
section
- both this and #2 are in the parent component, in the
- Include
v-on:...="$emit('...')"
in the HTML element that will communicate a user's response back to the parent (code snippet 5)- located in the child component
Long after this project is over, I might forget how exactly I emitted information from the Child component back to the parent, so I'll show code snippets of my code as examples for each of the numbered parts I explained in the section above.
- Variable declaration
selectedFriends
in the Vue component that will store the results of what we're listening for in the Child component. link to repo
export default {
components: {
InputText,
Friend
},
data() {
return {
... ,
selectedFriends: []
};
},
methods: {
...
}
};
- Where the results of the response from the Child component will be displayed. link
<p>{{ selectedFriends }}</p>
- The
FriendList
component listens forselect-friend
events from its childrenFriend
and runs the provided event handler"selectedFriends.push(friend.text)"
when it receives aselect-friend
event. link
<Friend
v-for="friend in friends"
:key="friend.id"
:name="friend"
v-on:select-friend="checkboxHandler(friend)"
/>
- Inside the Child component, the
v-on
listener will notify the parent in the event of aselect-friend
-- which occurs when the checkbox is clicked. link to repo
<input type="checkbox" id="checkbox" v-on:click="$emit('select-friend')" />
- Then, in a class method (which is called by
v-on
modifier in parent), I saved the names of the friends who were checked in this array calledselectedFriends
using this method:
/* @param: instance of friend; {id: someNum, text: "name"}
* returns: name of friend added/removed from selectedFriends list
*/
checkboxHandler(friend) {
// if name is in list, remove name
if (this.selectedFriends.find(element => element === friend.text)) {
/* why is it so complicated to remove an item from an array?
splice requires 1) index of thing you're removing
2) num of things you're removing*/
this.selectedFriends.splice(
this.selectedFriends.indexOf(friend.text),
1
);
} else {
this.selectedFriends.push(friend.text);
}
}
Problem: I had difficulty allowing for the exchange of data between parent and child components using emitting. As demonstrated above, I had to write a function in the parent TransactionList
to both save and remove the name from the array selectedFriends
, and I felt like Vue should have an easier way to do all of this.
I could achieve this behavior by using the sync
modifier and a getter and setter in the computed
part of the child component. From StackOverFlow
TL;DR of what's happening in the code:
.sync
is used in the parent component while calling the child, and cats
is the temporary array that stores the emitted responses from the child. Inside the child's method, the child does (update:cats
, [info that child component is passing back to the parent]).
< SplitBetw :cats.sync="selectedFriends">
// inside child component, method section:
this.$emit('update:cats, newValue);
The long explanation, with examples of this phenomenon in my code to follow.
- While passing the array to the child component, an additional .sync modifier is used on
cats
, a temporary array that stores the emitted responses from the child. The responses are then safely stored inselectedFriends
.
<SplitBetw
v-for="friend in friends"
:key="friend"
:name="friend"
:cats.sync="selectedFriends"
/>
:key
is used so that Vue can understand how to loop and go through your array. Typically, key is the index or a unique name.- I used
:name
to change friend into name on the child-level, so that the component usesname
to refer to a single friend. The variablefriend
does not exist on the child-level, even though it does on the parent. selectedFriends
is an array in the parent that will store the emitted responses (checked checkboxes in this case) from child. It is found withindata()
ofTransactionList
.
- Moving onto the child component, the
input
tag for the checkbox requires thev-model
to be set to a function defined in thecomputed
section of the component. I named this functionupdateCheckbox
, and it is under this function where the getter and setter is defined.
<label for="{name}">
<input
v-model="updateCheckbox"
type="checkbox"
id="{name}"
:value="{ name }"
/>
{{ name }}
</label>
- The getter and setter. Again,
cats
is a temporary place to store the emitted value the child will send back to parent. Remember to includecats
in prop!
export default {
props: {
// return selected checkboxes to selectedFriends
cats: Array,
...
}
},
computed: {
updateCheckbox: {
get() {
return this.cats;
},
set(newValue) {
this.$emit("update:cats", newValue);
}
}
}
};
- Going back to the
input
tag declaration, I had a bug where clicking on one checkbox checked them all, and it was fixed by including the:value = {name}
modifier! So don't forget it.
In the parent(?) definition of the Vue component, data must be returned as a function so that both the parent and child instances can retain an independent copy of the returned data object.
node_modules are build packages generated by Vue for my project and can grow to an astronomical size, hence this joke shared by Derrick:
Therefore, it's important to include node_modules in your .gitignore file, but if you already committed the node_modules file before adding the .gitignore, you need to untrack the directory and remove it from git, while keeping the file
git rm -r --cached node_modules
God were they unexpectedly difficult to get right ^^;
I used many components to organize my data, so the parent component always passed a string (the name of the friend) to the checkbox components, so that the checkbox components would generate the checkboxes themselves. (see the relationship between TransactionList
and SplitBetw
)
However, this meant the the child component, SplitBetw, had to emit the checked checkboxes back to the parent. This emitting behavior is basically the child component telling the parent which checkbox has been clicked.
By this stage of development, my project has started to have quite a few components, and with many components requires more knowledge of how to pass data between components.
When re-using components, for example in my project when I re-used the SplitBetw
component to display the list of checkboxes selected from both the "Split Between" and "Payers" section. For example, I have the following friends to choose from each section:
{"Split Between": [Derrick, Bunbun]}
{"Payers": [Derrick, Bunbun]}
Since I had set the id
of the checkbox input in SplitBetw
to simply be the name (ie. "Derrick"), clicking on the checkbox for "Split Between"'s Derrick also checked the box for "Payer"'s Derrick. Therefore, the id
needed to not only contain the name but the section ("Split Between" or "Payers") as well.
Problematic:
<input type="checkbox" :id="name" ... />
<label :for="name">
{{ name }}
</label>
Fixed bugs (for now):
Out in the parent component, the name
and type
are passed in:
<h2>Payers</h2>
<SplitBetw
v-for="friend in friends"
:name="friend"
type="payers"
:key="friend"
:returnedCheckboxes.sync="selectedPayers"
/>
To ensure that the child component SplitBetw
knows what those two values name
and type
are, make sure to include them in the props
section of the Vue instance!
export default {
props: {
// sync-ed with parent, parent receives an arr selectedFriends
returnedCheckboxes: Array,
// local re-name of "friend", so the ids can be unique across diff iterations of SplitBetw component
name: String,
type: String
},
...
};
In the child component, the html portion:
<!-- now, ids are unique across diff iterations of component-->
<input
:id="name + type"
v-model="updateCheckbox"
type="checkbox"
:value="name"
/>
<label :for="name + type">
{{ name }}
</label>
v-model
syncs the checkbox with the function updateCheckbox
. Thus, every time a certain checkbox is clicked or unclicked, this function updates the returnedCheckboxes
array that returns the list of friends selected from this component.
This is why the :value
of the checkbox must be set to the name of the friend, as the getter and setter use value
to add/remove the name from the returnedCheckboxes
array that is emitted back up to the parent component, InputForm
Up until this point, I had used v-model to save an input value to an initialized variable in the Vue instance, such as these code snippets from InputForm
<input id="expense" v-model="yourExpense" ... />
export default {
...
data() {
return {
...
yourExpense: "",
}
},
methods: {
checkForErrors:{
...
// then do something with the input u got
this.yourExpense
}
}
};
However, it was the first time I saw v-model used to trigger a function like here. please look above in the previous section for the example! it's really interesting.
By default, v-model
uses value
as a prop! This can be modified, however
I found this blog while working with nested Vue components that was sooo useful!
Apparently, it's a computed setter
"which allows us to both receive a dynamic value as well as call a function when we attempt to change that value." - zaengle blog
?? still have many questions. get to this later LOL
HOWEVER the example noted in the docs doesn't work for our use case.
NO!! :
computed: {
localForm: {
get() { return this.value },
set(localForm) {this.$emit('input', localForm)}
}
"v-html
should only used when you want to render HTML content from a variable"
When switching between the "Friends", "Transaction", and "Calculate Owed" sections of the web app, I implemented tabs. Each of the components I just listed occupied a page on the tab. The key to easily implementing toggle-able tabs using Vue is using dynamic Components:
- binding the
is
attribute that's set equal to - a computed method (maybe named
currentTabComponent()
?) that returns the active page/component that we'll be viewing v-bind
to assign any props the component takes in that's set equal to- a computed method to return the properties
:class
binding to assign a component as active at the response to a button- Therefore, the active component would have the following class binding:
<div class="tab-button active"></div>
- Thus, it's possible to style the active component in the css file using the tag name
.active { color: red; }
v-on:click
to assign a tab as clicked on clickdata
variable (perhaps namedactiveTab
?) to store the name of the tab that's open
<button
v-for="tab in tabs"
:key="tab"
class="tab"
:class="['tab-button', { active: tab === activeTab }]"
@click="activeTab = tab"
>
<span class="tab-copy">{{ tab }}</span>
<!-- TODO: handle this later
<span class="tab-background">
<span class="tab-rounding left"></span>
<span class="tab-rounding right"></span>
</span> -->
</button>
<component
v-bind:is="currentTabComponent"
v-bind="currentProperties"
class="tab"
></component>
Here is a StackOverflow that was particularly useful for learning when to use the computed
property for dynamic components.
We can use v-bind:class
to dynamically toggle between classes. I used the :class
binding to set a tab as the active one.
That I referenced!
I learned this from this StackOverFlow post that when emitting a value from the child component, in the parent component, you can:
- Bind the emitted variable to a parent-level property like this:
<InputForm :friends="friends" :emit-form.sync="newTransaction" />
so that the emitted localForm
object (from the child using this call: this.$emit('update:emit-form', this.localForm);
) is saved to the parent-level property newTransaction
. You can then take this newTransaction
property and add it to the list of transactions you have in the component, OR
- Directly use
v-on
to listen for the child-level emit eventupdate:emit-form
to be called in the childInputForm
and call the parent-level methodaddTransaction
.
<InputForm :friends="friends" @update:emit-form="addTransaction" />
Conveniently, the second field of the emit statement used in the child (so this.localForm
) is the parameter of the method you will declare in parent!
methods: {
// @param: returned form data from InputForm
addTransaction(newTransaction) {
console.log("Adding a transaction");
this.transactions.push(newTransaction);
}
}
So in this method, you can directly add the emitted object from child into the list transactions
in parent! This is a better solution for this situation compared to the first because the 1st only handles passing the form object from the child, whereas the second handles both passing the object and triggering a method to add the object to the list transactions
.
I've been getting this intriguing type error recently when I duplicated the InputText
component and made one to take a numerical input for cost
. In InputNum
, whenever I submitted the form, an error message indicating that "Expected Number, got String" shows up, referencing the parent component InputForm
in this line:
<InputNum v-model="localForm.expense" />
In the child component InputNum
, I was confused because the expected input type is indeed a number
<template>
<label>
Cost
<br />
<input
type="number"
:value="value"
v-on="listeners"
/>
...
</template>
and what is emitted is the input
export default {
props: {
value: {
type: Number
}
},
...
computed: {
listeners() {
return {
// Pass all component listeners directly to input
...this.$listeners,
// Override input listener of the same name from v-on
input: event => this.$emit("input", event.target.value)
};
}
}
So, I originally fixed the error by converting the input
to a number, assuming that somehow the input is a string for some reason.
// change:
input: event => this.$emit("input.Number()", event.target.value);
This removed the error message "Expected Number, got String" but a new issue arose. Now, whenever the form was submitted in InputForm
, the field for expense would just reset to 0. input.Number()
was causing an issue.
In the end, StackOverflow came to the rescue again. Suffixing with v-model.number
in the parent ensures that the emitted result is parsed as number. This proved to be less buggy that trying to change the input
into a number in the child, as done previously.
<InputNum v-model.number="localForm.expense" />
I had been following this blog post on how to toggle hovering over
components on Vue to toggle visibility of an edit name button next to the name itself. The how-to: In the parent component FriendList
, I included v-on
(the @) to listen to the child component for a new edited friend name, if any.
<ol>
<Friend
v-for="friend in friends"
:key="friend"
:name="friend"
@update:emit-name="editName"
/>
</ol>
Inside the child component Friend
, each for
loop passes in a friend name that needs to be created into a list item. I used v-on
to listen for when the mouse hovers over the specific list item, like "1. Derrick", and this toggles the visibility of the edit
button.
``html
hover
and edit
are both booleans I initialized in this component to help me keep track of which elements I want to show.
So far, I've only used v-if
, but the difference (described in the docs) is that event listeners and components are actually created and destroyed on toggle with v-if
. On the other hand, elements tagged with v-show
are always rendered, with css-based toggling.
"Generally speaking, v-if has higher toggle costs while v-show has higher initial render costs. So prefer v-show if you need to toggle something very often, and prefer v-if if the condition is unlikely to change at runtime."
Therefore, I used v-show
to toggle the edit button in Friend.vue
that becomes visible on hover, and v-if
for editing the name, as the components for that operation would only be generated when the user clicks on the "edit" button, and not on render.
So far, my not finalized font choices are
- Biorhyme for headings
- I found these font recommendations from this blog post
- Varela Round for the body
- Cute Font, my most questionable choice, is for displaying restaurant names in the boba balls
Following this StackOverFlow post, I was able to import functions from a local js file in order to move the error checking functions out to an external file so I don't have to look at it while debugging component stuff!
To do this, I must export the error checking functions as an object that I named formTests
let formTests = {
formIsFilled() {
// error checking to see if all form fields are filled
}
};
export default formTests;
This all was written in a file I named "errorChecks.js"
Then in the Vue component I wish to use these functions in, I must include the relational file path and the name of the file that contains the js tests.
import formTests from "../assets/errorChecks.js";
where the file organization is as follows
- assets
-errorChecks
- components
- current Vue component
Then when I want to use the function formIsFilled
from another js file in the Vue component, I simply call
formTests.formIsFilled();
- Use a container to wrap elements in a row.
- containers are the bread and butter of flexbox. My naming scheme for these wrappers are ___Container
- Without containers, you can't align
align-items
to vertically align html elements. See ex- Use
justify-content
to determine space between each other. See examples- Most notable example is like when u write a diary entry, maybe u have the title aligned left, and the date aligned right on the same line. You'd use
justify-content: space-between
on the container wrapping those two<span>
elements to display them on opposite ends
- Most notable example is like when u write a diary entry, maybe u have the title aligned left, and the date aligned right on the same line. You'd use
- Use
column-gap
within a container to put space between elements
- ex:
.buttonsContainer {
margin: 0 1rem; /*space between elements*/
display: flex;
column-gap: 0.5rem;
}
When collecting text inputs, I wanted to animate the bottom of the input box expanding when the user focuses on the text box. Visualizing the behavior, this is an unselected text box vs a text box that's being typed in
Name Name
____ Feli
--------
note about focus that I initially made a mistake with: "focus" is a state only applied to inputs, so I couldn't focus
on a <div>
, only a button, text box, and etc.
Inside InputText.vue
, the html for the input text box, I need a container (in this case, it is the label
) that encapsulates everything.
<label :for="question">
Name
<br />
<input type="text" id="question" />
<!-- styling for the line is found in Stylesheet.css -->
<span class="line"></span>
</label>
Thus, when the input
is focused, the label and line both need to respond by:
- changing color to pink and
- line needs to be animated traveling down.
To achieve this behavior, Derrick suggested that I use the adjacent sibling combinator +
to indicate that I'd like to style the second html element when the first element is focused.
/* adjacent sibling combinator in use
animation for focus enter */
#name:focus + .line {
transition: transform 125ms;
transform: translateY(10px);
border-color: #f09381;
}
/* animation for when focus leaves */
.line {
will-change: transform;
transition: transform 450ms;
}
StackOverflow post that helped me figure out which combinator to use.
To animate the label, I took advantage of focus-within
.
I initially ran into an obstacle while animating this because the focus element, the text input, is sandwiched between the label and line, which both required css animations. However, adjacent combinators like +
and ~
discussed only serve to style elements that come after the focused element. Therefore the label can't be animated with simply those combinators.
To solve this, I moved the input and line elements into the <label>
element (since i remembered that u could treat the label as a wrapper), so the label is now the container holding the <input>
. This works, because focus-within
styles the container when the input element inside is focused.
Therefore, the css animating the label (when the label is the container) is
label:focus-within {
color: #f09381;
}
What happens when I use a component for two different instances, once on white background, and another on black background, and the black text cannot be seen on black background?
Check this issue for an image showing this problem!
The font color depended on a conditional, and how would you implement this in Vue?
In my two instances of InputForm
, the empty form was on white background, and modify-able, filled out form on black background. Thus, to determine whether the font should be black or white, I needed to know whether the form was empty or not.
Fortunately, upon creation of a form, my initEdits()
method executes only when the form is filled in advance, so I added to this method, directly changing the font color to white
.
initEdits(){
// if form is filled
if(this.autoFillFormData){
...
/* directly manipulate InputText to change font color
of input to white so it can be seen on black background */
this.$refs.busnInput.changeColor();
}
}
In the parent, I marked the child component I'm going to be directly modifying with a ref
<InputText v-model="localForm.name" question="Business name" ref="busnInput" />
Inside the child, since I wanted to change the font color of the <input>
element only and nothing else, I also marked the input element with a ref
<label>
{{ question }}
<br />
<input type="text" ... ref="input" />
</label>
note: Notice that the ref
names are different!
Then, I defined in the child component InputText
a method that the parent will call to change the color of the <input>
element to white.
methods: {
changeColor: function() {
this.$refs.input.style.color = "white";
}
}
Then, back in the parent InputForm
, we can use
this.$refs.busnInput.changeColor();
to call the child component's method from the parent and change the font color to white!
Up until now, I've been making mock-ups in Adobe Photoshop and I've only used hex notation (ie. #ff246) to indicate color. While following a tutorial, however, I realized that there are 2 other ways to describe the same color. I can use
- RGB -> red, green, blue values
rgb(240, 147, 129)
- HSL -> hue, saturation, lightness
hsl(340deg 100% 32%)
To switch between these, I can use the various color converters and color pickers available on the internet! I just wanted to save the different names of how to describe all these colors here in case I need them in the future.
The method described above is what I originally used, and it works well, but I'm not sure if it's best practice, since I didn't really see refs
used in the beginner's docs of Vue. Style bindings seem like the more elegant and less hacky (methods reminiscient of older forms of web development without frameworks where devs were forced to modify html and css directly through js), and most of all, more modern.
context My app is divided into 3 tabs: Friends, Transaction, and Calculate, and I wanted to underline the tab with pink if the tab is open. View the full code
explanation of code
Each of the tabs
are named so, and have the normal styling class tab-button
of grey text and grey underline applied. When the button is clicked, the clicked button becomes the active tab, and the @click
toggles this behavior for us. So far, the code is within what a beginner html and css developer can expect :)
<button
v-for="(tab, index) in tabs"
:key="index"
class="tab-button"
@click="activeTab = tab"
></button>
Now moving on to the new info, the underline that we're going to conditionally style pink depending on whether the tab is active or not is as follows:
<span class="tab-front" :style="tab === activeTab && styleActive"
>{{ tab }}</span
>
Using :style
, the first expression tab === activeTab
evaluates whether the tab will apply the special style of the second, styleActive
. This blog post describes this as a guard expression using a logical AND to apply styleActive
if the boolean tab === activeTab
evaluates to true.
styleActive
is a computed method that returns the css as an object that we will be conditionally applying. Make sure to include the Vue syntax of separating each item with a comma (,) instead of a semi-colon like in css, and attributes with dashes need to be wrapped in quotes!
export default {
computed: {
styleActive() {
return {
"border-bottom": "5px solid #f09381",
color: "black"
};
}
}
References
- I followed this helpful blog post on the best practices of using class bindings and applied the similar concepts for style.
- an example of the usage of style bindings from StackOverflow was helpful in figuring out the exact syntax. In particular, when modifying
css
, I learned from this post that css styles need to be wrapped by quotations (ie.'background-color' : 'black'
), otherwise Vue wouldn't know what it is - this StackOverFlow post helped me realize that the syntax
{active: ____}
expects a boolean in the blank. Therefore, it can be a variable that is saved as a boolean, like "isActive", or an expression that evaluates to a boolean, like myactiveTab === tab
( I didn't really know how this worked bc I copied this from some expert's code and didn't get it for the longest time) - this was the most helpful StackOverflow post! It showed me that these style bindings could be saved as objects and toggled as a
computed method
. Before, I didn't know what the difference between computed methods and normal methods were ( I think i still don't really know), but I saw how they could be used in this way, and it was really eye-opening
I had no idea how to program sliding transitions dependent on the direction of how the tabs are switched until I stumbled upon this StackOverflow post. Unfortunately, though, I had a hard time understanding the official Vue documentation for Dynamic transitions, so perhaps I can improve on this explanation someday.
Since the direction the tabs will slide will depend on whether the tab indices will be changing from an ascending or descending value, I wrote a helper function to determine the direction of the slide transition
data() {
return {
/* starts at 0 bc the first tab's index is 0*/
previousTab: 0,
transitionLeft: true
}
},
methods: {
findTabDirection(newIndex) {
console.log(`${this.previousTab} -> ${newIndex}`);
if (this.previousTab < newIndex) {
console.log("shift left");
this.transitionLeft = true;
} else {
console.log("shift right");
this.transitionLeft = false;
}
this.previousTab = newIndex;
}
}
I needed previousTab
to keep track of the last index to determine the direction, and transitionLeft
is the boolean value that is absolutely crucial for the dynamic transition because it determines which direction, so therefore, which transition I'll be using.
TODO: continue explaining the transitions in Animations.css
I just want to put this somewhere to demonstrate how I can write a function that does the same thing differently.
Here, I'm writing a function that checks if a newly entered name already exists in the list of friends. To ensure that "Derrick" and "derrick" are not considered different names because of case, I used the string member function String.toLowerCase()
to change the name to lowercase before evaluating.
This is what I wrote a couple months ago using .some()
isDuplicate(name) {
return this.friends.some(
friendName => friendName.toLowerCase() === name.toLowerCase()
); // true if name is in friends
}
Today, on May 1, 2021, I wrote this:
isDuplicateF(friends, newName) {
return friends
.map(friend => friend.toLowerCase())
.includes(newName.toLowerCase()); // true if name is in friends
}
The new one I wrote is kinda verbose and less intuitive. Not readable. I think i'll use the old one!