Initial commit

This commit is contained in:
Gryffin Winkler 2024-05-23 11:40:53 -04:00
commit 8a1acb4bd0
33 changed files with 851 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

14
Contributing.md Normal file
View File

@ -0,0 +1,14 @@
# Contributing to Apress Source Code
Copyright for Apress source code belongs to the author(s). However, under fair use you are encouraged to fork and contribute minor corrections and updates for the benefit of the author(s) and other readers.
## How to Contribute
1. Make sure you have a GitHub account.
2. Fork the repository for the relevant book.
3. Create a new branch on which to make your change, e.g.
`git checkout -b my_code_contribution`
4. Commit your change. Include a commit message describing the correction. Please note that if your commit message is not clear, the correction will not be accepted.
5. Submit a pull request.
Thank you for your contribution!

27
LICENSE.txt Normal file
View File

@ -0,0 +1,27 @@
Freeware License, some rights reserved
Copyright (c) Eric Sarrion 2024
Permission is hereby granted, free of charge, to anyone obtaining a copy
of this software and associated documentation files (the "Software"),
to work with the Software within the limits of freeware distribution and fair use.
This includes the rights to use, copy, and modify the Software for personal use.
Users are also allowed and encouraged to submit corrections and modifications
to the Software for the benefit of other users.
It is not allowed to reuse, modify, or redistribute the Software for
commercial use in any way, or for a users educational materials such as books
or blog articles without prior permission from the copyright holder.
The above copyright notice and this permission notice need to be included
in all copies or substantial portions of the software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS OR APRESS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

16
README.md Normal file
View File

@ -0,0 +1,16 @@
# Apress Source Code
This repository accompanies [*Master Vue.js in 6 Days*](https://www.link.springer.com/book/10.1007/979-8-8688-0364-2) by Eric Sarrion (Apress, 2024).
[comment]: #cover
![Cover image](979-8-8688-0363-5.jpg)
Download the files as a zip using the green button, or clone the repository to your machine using Git.
## Releases
Release v1.0 corresponds to the code in the published book, without corrections or updates.
## Contributions
See the file Contributing.md for more information on how you can contribute to this repository.

11
Source Code/src/App.vue Normal file
View File

@ -0,0 +1,11 @@
<script setup>
import MyCounter from "./components/MyCounter.vue"
</script>
<template>
<MyCounter />
</template>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,58 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@ -0,0 +1,32 @@
<script setup>
import useToggleTheme from '../composables/useToggleTheme.js';
const [toggleTheme, themeStyles] = useToggleTheme(
{color:"red", backgroundColor:"black"},
{color:"green", textAlign:"right", backgroundColor:"gainsboro"}
);
</script>
<template>
<h3>MyCounter Component</h3>
<div>
<button @click="toggleTheme">Toggle Theme</button>
<p :style="themeStyles">Paragraph 1</p>
<p :style="themeStyles">Paragraph 2</p>
</div>
</template>
<style scoped>
p {
height : 30px;
padding-top: 10px;
padding-left: 10px;
padding-right: 10px;
}
</style>

View File

@ -0,0 +1,15 @@
<script setup>
import MyCounter from "./MyCounter.vue";
import { defineProps } from "vue";
const props = defineProps(["nb"]);
const nb = props.nb || 1;
</script>
<template>
<MyCounter v-for="i in nb" :key="i" :index="i" />
</template>

View File

@ -0,0 +1,25 @@
<script setup>
import useFetchCountries from "../composables/useFetchCountries"
import { ref } from "vue";
const data = ref();
const url = "https://restcountries.com/v3.1/all";
const [startFetch] = useFetchCountries(url);
const initData = async () => {
data.value = await startFetch();
}
</script>
<template>
<button @click="initData">Start Fetch</button>
<br><br>
<b>Data</b> :
<ul>
<li v-for="(country, i) in data" :key="i">{{country}}</li>
</ul>
</template>

View File

@ -0,0 +1,26 @@
<script setup>
import { ref } from "vue"
const name = ref("");
</script>
<template>
<h3>Input Form</h3>
Name: <input type="text" v-model.lazy="name" />
<br/><br/>
<h3>Reactive Variables</h3>
name: <b>{{name}}</b>
<br/><br/>
</template>
<style scoped>
h3 {
background-color: gainsboro;
padding: 5px;
}
</style>

View File

@ -0,0 +1,14 @@
import { ref } from "vue";
const useCounter = (init) => {
const count = ref(init);
const increment = () => {
count.value++;
}
const decrement = () => {
count.value--;
}
return [count, increment, decrement];
}
export default useCounter;

View File

@ -0,0 +1,16 @@
import useCounter from "../composables/useCounter";
const useCounterMax = (init, max) => {
const [count, increment, decrement] = useCounter(init);
const incrementMax = () => {
if (count.value >= max) {
return; // Avoid incrementing
}
else {
increment(); // Increment
}
}
return [count, incrementMax, decrement];
}
export default useCounterMax;

View File

@ -0,0 +1,28 @@
import useCounter from "../composables/useCounter";
import { ref, onMounted } from "vue";
const useCounterMaxWithError = (init, max) => {
const [count, increment, decrement] = useCounter(init);
const error = ref("");
const incrementMax = () => {
if (count.value >= max) {
error.value = "Maximum value reached!";
}
else {
increment();
error.value = "";
}
}
const decrementMax = () => {
decrement();
if (count.value <= max) {
error.value = "";
}
}
onMounted(()=> {
if (count.value > max) error.value = "Maximum value reached!";
});
return [count, incrementMax, decrementMax, error];
}
export default useCounterMaxWithError;

View File

@ -0,0 +1,10 @@
const useFetch = (url) => {
const startFetch = async () => {
const res = await fetch(url);
const d = await res.text();
return JSON.parse(d); // Returning the data read from the server in JSON format
}
return [startFetch];
}
export default useFetch;

View File

@ -0,0 +1,17 @@
import useFetch from "./useFetch";
const useFetchCountries = (url) => {
const [startFetch] = useFetch(url);
let countries;
const startFetchCountries = async () => {
const data = await startFetch();
countries = data.map(function(elem) {
return elem.name.common; // Retain only the common.name property
});
countries = countries.sort((n1, n2) => (n1 > n2)); // In ascending alphabetical order
return countries;
}
return [startFetchCountries];
}
export default useFetchCountries;

View File

@ -0,0 +1,34 @@
import { customRef } from "vue";
const formatDate = (date, format) => {
const options = { year: 'numeric', month: '2-digit', day: '2-digit' };
if (format == "MM-DD-YYYY")
return date.toLocaleDateString('en-US', options).replace(/\//g, '-');
else if (format == "DD-MM-YYYY")
return date.toLocaleDateString('en-GB', options).replace(/\//g, '-');
else if (format == "MM/DD/YYYY")
return date.toLocaleDateString('en-US', options);
else if (format == "DD/MM/YYYY")
return date.toLocaleDateString('en-GB', options);
}
const useFormatDate = (date, format) => {
return customRef((track, trigger) => {
let value = date; // value will be the tracked variable
return {
get() {
// track the dependency when the value is read
track();
return formatDate(value, format);
},
set(newValue) {
// update the value and trigger reactivity
value = newValue;
trigger();
}
};
});
};
export default useFormatDate;

View File

@ -0,0 +1,27 @@
import { ref, onMounted } from "vue";
const useGeolocation = () => {
const latitude = ref(null);
const longitude = ref(null);
const handleGeolocation = (position) => {
latitude.value = position.coords.latitude;
longitude.value = position.coords.longitude;
};
const errorGeolocation = (error) => {
console.log("Geolocation error:", error.message);
};
onMounted(() => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(handleGeolocation, errorGeolocation);
} else {
console.log("Geolocation is not available in this browser.");
}
});
return [latitude, longitude];
}
export default useGeolocation;

View File

@ -0,0 +1,29 @@
import { ref, watchEffect } from "vue";
import useGeolocation from '../composables/useGeolocation';
const useGeolocationWithDetails = () => {
const [latitude, longitude] = useGeolocation();
const country = ref("");
const city = ref("");
// To find the country and city corresponding to the latitude/longitude
watchEffect(async ()=>{
if (latitude.value && longitude.value) {
const response = await fetch(
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude.value}&lon=${longitude.value}`
);
const data = await response.json();
if (data && data.address && data.address.country) {
country.value = data.address.country;
}
if (data && data.address) {
city.value = data.address.city || data.address.town;
}
}
});
return [latitude, longitude, country, city];
}
export default useGeolocationWithDetails;

View File

@ -0,0 +1,21 @@
import L from "leaflet"
const useMap = (latitude, longitude, idMap) => {
const zoom = 13;
// To position the map at the indicated location
const map = L.map(idMap).setView([latitude, longitude], zoom);
// To display the corresponding map
L.tileLayer("https://a.tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 20,
}).addTo(map);
// To display a marker on the map to indicate the specified location
L.marker([latitude, longitude]).addTo(map);
// We return the map object created by Leaflet
return map;
}
export default useMap;

View File

@ -0,0 +1,22 @@
import { customRef } from "vue";
const useMaximum = (max) => {
// Create a custom reference (customRef)
return customRef((track, trigger) => {
let value = 0; // value will be the variable being tracked, initialized here to 0.
return {
get() {
// Track the dependency when the value is read.
track();
return value;
},
set(newValue) {
// Update the value and trigger reactivity.
if (newValue <= max) value = newValue;
trigger();
}
};
});
}
export default useMaximum;

View File

@ -0,0 +1,17 @@
import { ref } from 'vue';
const useThemeToggle = (theme0, theme1) => {
const themes = [theme0, theme1];
let theme = 0; // theme0 by default
const themeStyles = ref(themes[theme]);
const toggleTheme = () => {
if (theme == 0) theme = 1;
else theme = 0;
themeStyles.value = themes[theme];
};
return [toggleTheme, themeStyles];
}
export default useThemeToggle;

View File

@ -0,0 +1,21 @@
import { customRef } from 'vue';
const useUpperCase = (initValue) => {
return customRef((track, trigger) => {
let value = initValue; // value will be the tracked variable
return {
get() {
// track the dependency when the value is read
track();
return value.toUpperCase();
},
set(newValue) {
// update the value and trigger reactivity
value = newValue;
trigger();
}
};
});
};
export default useUpperCase;

View File

@ -0,0 +1,27 @@
import { ref, onMounted, onBeforeUnmount } from "vue";
const useWindowSize = () => {
const windowSize = ref({
width: window.innerWidth,
height: window.innerHeight,
});
const updateWindowSize = () => {
windowSize.value = {
width: window.innerWidth,
height: window.innerHeight,
};
};
onMounted(() => {
window.addEventListener('resize', updateWindowSize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', updateWindowSize);
});
return windowSize;
}
export default useWindowSize;

View File

@ -0,0 +1,18 @@
import focus from "./directives/focus";
import integersOnly from "./directives/integers-only";
import maxValue from "./directives/max-value";
import clearable from "./directives/clearable";
import timer from "./directives/timer";
import map from "./directives/map";
import color from "./directives/color";
export default {
focus,
integersOnly,
maxValue,
clearable,
timer,
map,
color,
}

View File

@ -0,0 +1,24 @@
const clearable = {
mounted(el) {
const clearButton = document.createElement("button");
clearButton.innerHTML = "Clear";
clearButton.style = "position:relative; left:10px;";
// Handle the click on the button (clear the content of the input field).
clearButton.addEventListener("click", () => {
// Clear the content of the input field.
el.value = "";
// Simulate an input event to mimic a keyboard key press
// (mandatory to ensure that the reactive variable linked to the input field is updated)
el.dispatchEvent(new Event("input"));
// Give focus to the input field
el.focus();
});
// Insert the button after the input field
el.parentNode.insertBefore(clearButton, el.nextSibling);
}
};
export default clearable;

View File

@ -0,0 +1,17 @@
const color = {
mounted(el, binding) {
let colorStyle;
if (binding.modifiers.toggle) {
const colors = binding.value;
el.addEventListener("click", () => {
if (colorStyle == colors[0]) colorStyle = colors[1];
else if (colorStyle == colors[1]) colorStyle = colors[0];
else colorStyle = colors[0];
// Change the background color of the element
el.style.backgroundColor = colorStyle;
});
}
},
};
export default color;

View File

@ -0,0 +1,27 @@
const focusDirective = {
mounted(el, binding) {
const arg = binding.arg;
const value = binding.value;
// Position the handling of the focus and blur events
el.addEventListener("focus", () => {
if (arg == "color") el.style.color = value;
if (arg == "backgroundcolor") el.style.backgroundColor = value;
if (arg == "colors") {
el.style.color = value.color;
el.style.backgroundColor = value.backgroundcolor;
}
});
el.addEventListener("blur", () => {
if (arg == "color") el.style.color = "";
if (arg == "backgroundcolor") el.style.backgroundColor = "";
if (arg == "colors") {
el.style.color = "";
el.style.backgroundColor = "";
}
});
// and then give focus to the input field
el.focus();
}
};
export default focusDirective;

View File

@ -0,0 +1,56 @@
const integersOnly = {
mounted(el, binding) {
if (binding.modifiers.upper) {
// Convert the displayed field to uppercase with an initial value
// (for this to work, the v-model directive must be written before this one in the input field)
el.value = el.value.toUpperCase();
// Simulate an input event to mimic a keyboard keypress
// (necessary for the reactive variable linked to the field to be updated)
el.dispatchEvent(new Event("input"));
}
el.addEventListener("keydown", (event) => {
const numbers = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
const letters = ["a", "b", "c", "d", "e", "f", "A", "B", "C", "D", "E", "F"];
const moves = ["Backspace", "ArrowLeft", "ArrowRight",
"Delete", "Tab", "Home", "End"];
let authorized; // Allowed keys in the input field
// Allow hexadecimal characters if the hexa modifier is present
if (binding.modifiers.hexa) authorized = [...numbers, ...letters, ...moves];
else authorized = [...numbers, ...moves];
// If the key is not allowed, do not take it into account
if (!authorized.includes(event.key)) event.preventDefault();
// Handle the upper modifier
if (binding.modifiers.upper) {
// If the key is a hexadecimal letter, convert it to uppercase
if (letters.includes(event.key)) {
const start = el.selectionStart;
const end = el.selectionEnd;
const text = el.value;
// Insert the character at the cursor position
const newText = text.substring(0, start) + event.key + text.substring(end);
// Update the value of the input field (in uppercase)
el.value = newText.toUpperCase();
// Move the cursor after the inserted character
el.setSelectionRange(start + 1, start + 1);
// Prevent further processing of the key (as it has already been handled above)
event.preventDefault();
// Simulate an input event to mimic a keyboard keypress
// (necessary for the reactive variable linked to the field to be updated)
el.dispatchEvent(new Event("input"));
}
}
});
},
}
export default integersOnly;

View File

@ -0,0 +1,19 @@
import useMap from "../composables/useMap.js"
const map = {
updated(el, binding) {
const latitude = binding.value.latitude;
const longitude = binding.value.longitude;
if (latitude && longitude) {
if (el._map) el._map.remove();
el._map = useMap(latitude, longitude, el.id);
}
else if (el._map) {
el._map.remove();
el._map = null;
}
}
}
export default map;

View File

@ -0,0 +1,28 @@
const treatment = (el, binding) => {
const maxValue = binding.value || 100; // 100 by default
const value = el.value || 0; // Value in the field
const bold = binding.modifiers.bold;
if (value > maxValue) {
el.style.color = "red";
if (bold) {
el.style.fontWeight = "bold";
el.style.fontFamily = "arial";
}
}
else {
el.style.color = "";
el.style.fontWeight = ""; // Removal of "bold"
el.style.fontFamily = ""; // Removal of "arial"
}
}
const maxValue = {
mounted(el, binding) {
treatment(el, binding);
},
updated(el, binding) {
treatment(el, binding);
},
}
export default maxValue;

View File

@ -0,0 +1,141 @@
const timer = {
mounted(el, binding) {
const ms = binding.modifiers.ms;
const chrono = binding.modifiers.chrono;
// Initialization of the clock or stopwatch.
if (!chrono) {
let time = getCurrentTime(ms);
el.innerHTML = time;
}
else {
if (!ms) el.innerHTML = "00:00:00";
else el.innerHTML = "00:00:00.0";
}
setInterval(()=>{
if (!chrono) {
let time = getCurrentTime(ms);
el.innerHTML = time;
}
else {
const chronoTime = getChronoTime(ms);
el.innerHTML = chronoTime;
}
}, 100);
},
}
function getCurrentTime(ms = false) {
const now = new Date();
const hours = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
const seconds = now.getSeconds().toString().padStart(2, '0');
let formattedTime = `${hours}:${minutes}:${seconds}`;
if (ms) {
const milliseconds = now.getMilliseconds().toString().slice(0, 1); // Obtaining tenths of a second
formattedTime += `.${milliseconds}`;
}
return formattedTime;
}
let startChronoTime = new Date(); // Starting time of the stopwatch
function getChronoTime(ms = false) {
const now = new Date();
const elapsedMilliseconds = now.getTime() - startChronoTime.getTime();
const hours = Math.floor(elapsedMilliseconds / (3600 * 1000));
const remainingMilliseconds1 = elapsedMilliseconds % (3600 * 1000);
const minutes = Math.floor(remainingMilliseconds1 / (60 * 1000));
const remainingMilliseconds2 = remainingMilliseconds1 % (60 * 1000);
const seconds = Math.floor(remainingMilliseconds2 / 1000);
let formattedTime = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
if (ms) {
const milliseconds = Math.floor(remainingMilliseconds2 % 1000);
const tenthsOfSecond = Math.floor((milliseconds % 1000) / 100);
formattedTime += `.${tenthsOfSecond.toString()}`;
}
return formattedTime;
}
export default timer;
//~ const timer = {
//~ mounted(el, binding) {
//~ const ms = binding.modifiers.ms;
//~ const chrono = binding.modifiers.chrono;
//~ // Initialization of the clock or stopwatch.
//~ if (!chrono) {
//~ let time = getCurrentTime(ms);
//~ el.innerHTML = time;
//~ }
//~ else {
//~ if (!ms) el.innerHTML = "00:00:00";
//~ else el.innerHTML = "00:00:00.0";
//~ }
//~ setInterval(()=>{
//~ if (!chrono) {
//~ let time = getCurrentTime(ms);
//~ el.innerHTML = time;
//~ }
//~ else {
//~ const chronoTime = getChronoTime(ms);
//~ el.innerHTML = chronoTime;
//~ }
//~ }, 100);
//~ },
//~ }
//~ function getCurrentTime(ms = false) {
//~ const now = new Date();
//~ const hours = now.getHours().toString().padStart(2, '0');
//~ const minutes = now.getMinutes().toString().padStart(2, '0');
//~ const seconds = now.getSeconds().toString().padStart(2, '0');
//~ let formattedTime = `${hours}:${minutes}:${seconds}`;
//~ if (ms) {
//~ const milliseconds = now.getMilliseconds().toString().slice(0, 1); // Obtaining tenths of a second.
//~ formattedTime += `.${milliseconds}`;
//~ }
//~ return formattedTime;
//~ }
//~ let startChronoTime = new Date(); // Starting time of the stopwatch.
//~ function getChronoTime(ms = false) {
//~ const now = new Date();
//~ const elapsedMilliseconds = now.getTime() - startChronoTime.getTime();
//~ const hours = Math.floor(elapsedMilliseconds / (3600 * 1000));
//~ const remainingMilliseconds1 = elapsedMilliseconds % (3600 * 1000);
//~ const minutes = Math.floor(remainingMilliseconds1 / (60 * 1000));
//~ const remainingMilliseconds2 = remainingMilliseconds1 % (60 * 1000);
//~ const seconds = Math.floor(remainingMilliseconds2 / 1000);
//~ let formattedTime = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
//~ if (ms) {
//~ const milliseconds = Math.floor(remainingMilliseconds2 % 1000);
//~ const tenthsOfSecond = Math.floor((milliseconds % 1000) / 100);
//~ formattedTime += `.${tenthsOfSecond.toString()}`;
//~ }
//~ return formattedTime;
//~ }
//~ export default timer;

12
Source Code/src/main.js Normal file
View File

@ -0,0 +1,12 @@
import { createApp } from 'vue';
import App from './App.vue';
import directives from "./directives.js"
const app = createApp(App);
for (let name in directives) {
// Creation of the directive name within the application
app.directive(name, directives[name]);
}
app.mount('#app');