diff --git a/.eslintrc.json b/.eslintrc.json index 05aae6b..e1265f1 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -8,7 +8,7 @@ "jest": true }, "parserOptions": { - "ecmaVersion": 8 + "ecmaVersion": 2018 }, "rules": { "arrow-body-style": 2, @@ -30,7 +30,7 @@ "max-lines-per-function": ["error", 25], "max-nested-callbacks": ["error", 4], "max-params": ["error", 5], - "max-statements": ["error", 10], + "max-statements": ["error", 15], "no-cond-assign": 2, "no-dupe-args": "error", "no-dupe-keys": "error", diff --git a/04 DevOps.md b/04 DevOps.md index ea6d565..21841f8 100644 --- a/04 DevOps.md +++ b/04 DevOps.md @@ -118,4 +118,24 @@ Continuous integration is the process of regularly merging code changes into the Committing and pushing to trigger the workflow. -When running CI you need to run headless. +## 3 Continuous Delivery + +Once you have mastered the deployment process and configuring your CI pipeline you will want to implement a full CD workflow. This will require you to automate the deployment to one (or more) cloud servers. To achieve this you will need to add additional stages to your CI build to: + +1. Deploy to a test server. +2. Run a suite of acceptance tests. +3. Deploy to the live server. + +Whilst you will need to spend time configuring the build and ensuring that the correct stages of the pipeline run on the correct branches you will find the following snippet helpful: + +```yaml +deploy-staging-server: + stage: staging-server + script: + - apt-get update -qy + - apt-get install -y ruby ruby-dev rubygems-integration + - gem install dpl + - dpl --provider=heroku --app= --api-key= + only: + - master +``` diff --git a/05 The DOM.md b/05 The DOM.md index 661a714..afaa104 100644 --- a/05 The DOM.md +++ b/05 The DOM.md @@ -59,12 +59,37 @@ e.target.parentElement.firstChild.innerHTML ## 2 Form Validation +Now you have mastered DOM manipulation its time to move onto one of the most common uses of client-side JavaScript, form validation. The base code can be found in the `exercises/05_client/contacts/` directory. In the previous example we opened the `index.html` file using the `files://` protocol by dragging the html file into the chrome browser. Since we will be implementing some features that don't support this we need to access the page over `http://`. One way would be to create a koa script that serves static content however a quick and simple solution is to use the [`serve`](https://www.npmjs.com/package/serve) package. This needs to be installed and run from the `contacts/` directory. + +```javascript +$ npm install serve +$ ./node_modules/.bin/serve + ┌──────────────────────────────────────────────────┐ + │ │ + │ Serving! │ + │ │ + │ - Local: http://localhost:5000 │ + │ - On Your Network: http://10.17.114.20:5000 │ + │ │ + │ Copied local address to clipboard! │ + │ │ + └──────────────────────────────────────────────────┘ +``` + +Now you can paste the url into the chrome browser to access the page over http! Since the code is loaded by the browser you don't need to restart this server after making changes to the code. Remember to stop the server using `ctrl+c` when you have stopped working on the site. + Handling form validation Revealing sections of the form Preventing form submission +### 2.1 Data Persistence + +If you close your browser down and re-open it you will see that the contacts you added have been persisted! The data is stored within the browser using a document database techology called [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Basic_Concepts_Behind_IndexedDB). + +Viewing the data. + ## 3 Making API Calls In this section you will learn how to retrieve data from an API, specifically we will be covering: diff --git a/exercises/03_acceptance/todo/test.sh b/exercises/03_acceptance/todo/test.sh index 86123ae..d61e3a1 100755 --- a/exercises/03_acceptance/todo/test.sh +++ b/exercises/03_acceptance/todo/test.sh @@ -10,3 +10,6 @@ node index.js& node_modules/.bin/jest --runInBand --detectOpenHandles acceptance\ tests/* read -p "Press enter to continue" kill %1 + +# if you are running on unix and the node process is not being destroyed by the kill %1 command above then you can try: +#pkill node diff --git a/exercises/05_client/contacts/index.html b/exercises/05_client/contacts/index.html new file mode 100644 index 0000000..960478a --- /dev/null +++ b/exercises/05_client/contacts/index.html @@ -0,0 +1,42 @@ + + + + + + + + Contacts + + + + + + +
+
+

Contacts

+
+ +
+
+ +

+

+

+

+

+ + +

+
+
+
+
+ + diff --git a/exercises/05_client/contacts/script.js b/exercises/05_client/contacts/script.js new file mode 100644 index 0000000..7857011 --- /dev/null +++ b/exercises/05_client/contacts/script.js @@ -0,0 +1,101 @@ + +'use strict' + +const minNameLength = 3 +const minEmailLength = 10 + +document.addEventListener('DOMContentLoaded', async() => getAll()) + +function selectContact(e) { + // We access the id attribute of the link we clicked + const key = e.target.getAttribute('id') + // And use this to retrieve the relevant record + const data = JSON.parse(localStorage.getItem(key)) + // Now we insert the data into the form + document.querySelector('input[name="name"]').value = data.name + document.querySelector('input[name="email"]').value = data.email + document.querySelector('input[name="key"]').value = key + // now we change the text on the button and rewire the event handler + document.querySelector('button#submit').innerHTML = 'Update Record' + document.querySelector('button#submit').onclick = updateContact + // finally we reveal the delete button + document.querySelector('button#delete').classList.remove('hidden') + return false +} + +function updateContact() { + // we grab all the data from the form's input boxes into an object + const data = Array.from(document.querySelectorAll('input')).reduce( (acc, val) => { + if(val.name !== 'key') acc[val.name] = val.value + return acc + }, {}) + // next we get the record key stored in the hidden field + const key = document.querySelector('input[name="key"]').value + // and finally store the updated data using the same key + localStorage.setItem(key, JSON.stringify(data)) + // lastly we reload the contact list + getAll() + return false +} + +function deleteContact() { + // we get the record key stored in the hidden field + const key = document.querySelector('input[name="key"]').value + // and delete that object from localstorage + localStorage.removeItem(key) + // lastly we reload the contacts list + getAll() + return false +} + +function clearForm() { + // we delete the data from all the input boxes + document.querySelector('input[name="name"]').value = '' + document.querySelector('input[name="email"]').value = '' + // return the cursor focus to the name field + document.querySelector('input[name="name"]').focus() + // change the button text + document.querySelector('button').innerHTML = 'Add Record' + // wire the button to the add function + document.querySelector('button').onclick = addContact + document.querySelector('button#delete').onclick = deleteContact + // hide the delete button + document.querySelector('button#delete').classList.add('hidden') +} + +function addContact() { + if (document.querySelector('input[name="name"]').value.length < minNameLength) return false + if (document.querySelector('input[name="email"]').value.length < minEmailLength) return false + const data = Array.from(document.querySelectorAll('input')).reduce( (acc, val) => { + if(val.name !== 'key') acc[val.name] = val.value + return acc + }, {}) + const key = getHash() + localStorage.setItem(key, JSON.stringify(data)) + getAll() + return false +} + +function getAll() { + const items = {...localStorage} // spread operator to return all keys + const list = document.querySelector('ol') + list.innerHTML = '' + for(const key in items) { + const listItem = document.createElement('li') + const link = document.createElement('a') + link.onclick = selectContact + link.setAttribute('href', '#') + link.setAttribute('id', key) + link.innerHTML = JSON.parse(items[key]).name + listItem.appendChild(link) + list.appendChild(listItem) + } + clearForm() +} + +function getHash() { + const hashLen = 36 + const keyLen = 8 + const offset = 2 + return Math.random().toString(hashLen).substr(offset, keyLen) +} diff --git a/exercises/05_client/contacts/style.css b/exercises/05_client/contacts/style.css new file mode 100644 index 0000000..cd13711 --- /dev/null +++ b/exercises/05_client/contacts/style.css @@ -0,0 +1,58 @@ + +body { + font-family: verdana; +} + +table { + margin: 20px; + border-spacing: 0; +} + +td { + border: 1px solid grey; + margin: 0; + padding: 5px; +} + +header { + height: 120px; + background-color: #999; +} + +h1, h2 { + padding: 20px; +} + +h2 { + font-size: 1.2em +} + +/* button#delete { + visibility: hidden; +} */ + +.hidden { + visibility: hidden; +} + +form { + background-color: #666; + padding: 20px; + padding-left: 220px; +} + +main { + width: 800px; + background-color: yellow; + margin: auto; +} + +nav { + width: 200px; + background-color: grey; + float: left; +} + +article { + background-color: #ccc; +}