Added Base form elements. Was kind of light on the notes though

master
androiddrew 6 years ago
parent 19a7dd3f31
commit 6a58aa50b3

@ -2,6 +2,8 @@
## Course Take aways: ## Course Take aways:
- You missed it in the vuex course but they started using `import Datepicker from 'vuejs-datepicker'` in the EventCreate component.
- Installation of [NProgress](https://github.com/rstacruz/nprogress) to create a better experience for a user when they are interacting with our application. - Installation of [NProgress](https://github.com/rstacruz/nprogress) to create a better experience for a user when they are interacting with our application.
- Axios Interceptors - Axios Interceptors
- Not optimal for multiple API calls - Not optimal for multiple API calls
@ -37,6 +39,12 @@ beforeRouteLeave(routeTo, routeFrom, next) {
} }
``` ```
### Base Form elements
- Look up `vue-multiselect` for when you want to use a select field and the options have objects in them.
- `v-on="$listeners"` means that this element inherits the listners of the parent component
- `v-bind="$attrs"` won't work for inheriting class and style attributes
### Global Route Gaurds & Per Route Gaurds ### Global Route Gaurds & Per Route Gaurds
Global Route Gaurds allow us to add hooks that run whenever nagigation is triggered. These gaurds are called on the `router` object. We need to set the `router` object to a contant. Global Route Gaurds allow us to add hooks that run whenever nagigation is triggered. These gaurds are called on the `router` object. We need to set the `router` object to a contant.
@ -87,6 +95,10 @@ const router = new Router({
Since all the API action calls are occuring in the router lifecycle hooks at this time. You can actually remove all vuex code from the component. This could make your code simpler and easier to maintain. You will need to ensure that the actions in your vuex store are returning the objects you are working with however to ensure this works. Since all the API action calls are occuring in the router lifecycle hooks at this time. You can actually remove all vuex code from the component. This could make your code simpler and easier to maintain. You will need to ensure that the actions in your vuex store are returning the objects you are working with however to ensure this works.
_NOTE:_ Since `beforeEnter` is only called when the component is created it's use with a component that does paginatation will not work. The `fetchEvent` wouldn't be called.
_NOTE2:_ Vue Router may get another per-route guard called beforeResolve which called on Create and Update. If it is available it would be a more elegant solution to our pagination problem.
## Following along? ## Following along?
We encourage you to follow the course on Vue Mastery, and code along with us. This course has tags representing the start and finish of each level, just in case you get stuck. Here's the start and ending code of each lesson, if you'd like to download them. We encourage you to follow the course on Vue Mastery, and code along with us. This course has tags representing the start and finish of each level, just in case you get stuck. Here's the start and ending code of each lesson, if you'd like to download them.

@ -267,6 +267,24 @@
"date": "", "date": "",
"time": "", "time": "",
"attendees": [] "attendees": []
},
{
"id": 7184295,
"user": {
"id": "abc123",
"name": "Adam"
},
"category": "nature",
"organizer": {
"id": "abc123",
"name": "Adam"
},
"title": "Here we go again",
"description": "",
"location": "",
"date": "",
"time": "",
"attendees": []
} }
] ]
} }

@ -0,0 +1,86 @@
<template>
<div>
<button v-on="$listeners" v-bind="$attrs" class="button" :class="buttonClass">
<slot/>
</button>
</div>
</template>
<script>
// v-on="$listeners" means that this element inherits the listners of the parent component
export default {
inheritAttrs: false, // You still want this because it could but used for passing an attribute like disabled to your button
props: {
buttonClass: {
type: String
}
}
}
</script>
<style scoped>
.button {
display: inline-flex;
align-items: center;
justify-content: space-between;
height: 52px;
padding: 0 40px;
background: transparent;
border: none;
border-radius: 6px;
text-align: center;
font-weight: 600;
white-space: nowrap;
transition: all 0.2s linear;
}
.button:hover {
-webkit-transform: scale(1.02);
transform: scale(1.02);
box-shadow: 0 7px 17px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
.button:active {
-webkit-transform: scale(1);
transform: scale(1);
box-shadow: none;
}
.button:focus {
outline: 0;
}
.button.-fill-gradient {
background: linear-gradient(to right, #16c0b0, #84cf6a);
color: #ffffff;
}
.button:disabled {
-webkit-transform: scale(1);
transform: scale(1);
box-shadow: none;
background: #eeeeee;
}
.button + .button {
margin-left: 1em;
}
.button.-fill-gray {
background: rgba(0, 0, 0, 0.5);
color: #ffffff;
}
.button.-size-small {
height: 32px;
}
.button.-icon-right {
text-align: left;
padding: 0 20px;
}
.button.-icon-right > .icon {
margin-left: 10px;
}
.button.-icon-left {
text-align: right;
padding: 0 20px;
}
.button.-icon-left > .icon {
margin-right: 10px;
}
.button.-icon-center {
padding: 0 20px;
}
</style>

@ -0,0 +1,27 @@
<template>
<div>
<label v-if="label">{{ label }}</label>
<input :value="value" @input="updateValue" v-bind="$attrs">
</div>
</template>
<script>
export default {
inheritAttrs: false,
props: {
label: {
type: String,
default: ''
},
value: [String, Number]
},
methods: {
updateValue() {
this.$emit('input', event.target.value)
}
}
}
</script>
<style scoped>
</style>

@ -0,0 +1,33 @@
<template>
<div>
<label v-if="label">{{ label }}</label>
<select :value="value" @input="updateValue" v-bind="$attrs">
<option v-for="option in options" :key="option" :selected="option === value">{{ option }}</option>
</select>
</div>
</template>
<script>
export default {
inheritAttrs: false,
props: {
options: {
type: Array,
required: true
},
label: {
type: String,
default: ''
},
value: [String, Number]
},
methods: {
updateValue() {
this.$emit('input', event.target.value)
}
}
}
</script>
<style scoped>
</style>

@ -5,6 +5,8 @@ import EventList from './views/EventList.vue'
import EventShow from './views/EventShow.vue' import EventShow from './views/EventShow.vue'
import NProgress from 'nprogress' import NProgress from 'nprogress'
import store from '@/store/store' import store from '@/store/store'
import NotFound from './views/NotFound.vue'
import NetworkIssue from './views/NetworkIssue.vue'
Vue.use(Router) Vue.use(Router)
@ -15,8 +17,8 @@ const router = new Router({
{ {
path: '/', path: '/',
name: 'event-list', name: 'event-list',
component: EventList component: EventList,
// This path needs to call dispatch using beforeEnter like the event-show props: true
}, },
{ {
path: '/event/create', path: '/event/create',
@ -29,11 +31,35 @@ const router = new Router({
component: EventShow, component: EventShow,
props: true, props: true,
beforeEnter(routeTo, routeFrom, next) { beforeEnter(routeTo, routeFrom, next) {
store.dispatch('event/fetchEvent', routeTo.params.id).then(event => { store
routeTo.params.event = event .dispatch('event/fetchEvent', routeTo.params.id)
next() .then(event => {
}) routeTo.params.event = event
next()
})
.catch(error => {
if (error.response && error.response.status == 404) {
next({ name: '404', params: { resource: 'event' } })
} else {
next({ name: 'network-issue' })
}
})
} }
},
{
path: '/404',
name: '404',
component: NotFound,
props: true
},
{
path: '/network-issue',
name: 'network-issue',
component: NetworkIssue
},
{
path: '*',
redirect: { name: '404', params: { resource: 'page' } }
} }
] ]
}) })

@ -7,7 +7,8 @@ const apiClient = axios.create({
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} },
timeout: 10000 // ten seconds
}) })
// Interceptor definition. This is like middleware // Interceptor definition. This is like middleware

@ -5,7 +5,8 @@ export const namespaced = true
export const state = { export const state = {
events: [], events: [],
eventsTotal: 0, eventsTotal: 0,
event: {} event: {},
perPage: 3
} }
export const mutations = { export const mutations = {
@ -43,8 +44,9 @@ export const actions = {
throw error throw error
}) })
}, },
fetchEvents({ commit, dispatch }, { perPage, page }) { // perPage is removed from the payload because it is now part of the global state storage
EventService.getEvents(perPage, page) fetchEvents({ commit, dispatch, state }, { page }) {
return EventService.getEvents(state.perPage, page) // returning the promise
.then(response => { .then(response => {
commit('SET_EVENTS_TOTAL', parseInt(response.headers['x-total-count'])) commit('SET_EVENTS_TOTAL', parseInt(response.headers['x-total-count']))
commit('SET_EVENTS', response.data) commit('SET_EVENTS', response.data)
@ -57,7 +59,8 @@ export const actions = {
dispatch('notification/add', notification, { root: true }) dispatch('notification/add', notification, { root: true })
}) })
}, },
fetchEvent({ commit, getters, dispatch }, id) { fetchEvent({ commit, getters }, id) {
// dispatch was removed as a param because we cut out the catch error block below.
var event = getters.getEventById(id) var event = getters.getEventById(id)
if (event) { if (event) {
@ -69,13 +72,14 @@ export const actions = {
commit('SET_EVENT', response.data) commit('SET_EVENT', response.data)
return response.data return response.data
}) })
.catch(error => { // This was removed so that we could 404 any events that didn't exist. We want to catch the error in the beforeEnter hook instead
const notification = { // .catch(error => {
type: 'error', // const notification = {
message: 'There was a problem fetching event: ' + error.message // type: 'error',
} // message: 'There was a problem fetching event: ' + error.message
dispatch('notification/add', notification, { root: true }) // }
}) // dispatch('notification/add', notification, { root: true })
// })
} }
} }
} }

@ -2,27 +2,27 @@
<div> <div>
<h1>Create an Event</h1> <h1>Create an Event</h1>
<form @submit.prevent="createEvent"> <form @submit.prevent="createEvent">
<label>Select a category</label> <BaseSelect label="Select a category" :options="categories" v-model="event.category"/>
<select v-model="event.category">
<option v-for="cat in categories" :key="cat">{{ cat }}</option>
</select>
<h3>Name & describe your event</h3> <h3>Name & describe your event</h3>
<div class="field"> <BaseInput label="Title" v-model="event.title" type="text" placeholder="Title" class="field"/>
<label>Title</label>
<input v-model="event.title" type="text" placeholder="Add an event title"/>
</div>
<div class="field"> <BaseInput
<label>Description</label> label="Description"
<input v-model="event.description" type="text" placeholder="Add a description"/> v-model="event.description"
</div> type="text"
placeholder="Add a description"
class="field"
/>
<h3>Where is your event?</h3> <h3>Where is your event?</h3>
<div class="field"> <BaseInput
<label>Location</label> label="Location"
<input v-model="event.location" type="text" placeholder="Add a location"/> v-model="event.location"
</div> type="text"
placeholder="Add a location"
class="field"
/>
<h3>When is your event?</h3> <h3>When is your event?</h3>
@ -31,14 +31,10 @@
<datepicker v-model="event.date" placeholder="Select a date"/> <datepicker v-model="event.date" placeholder="Select a date"/>
</div> </div>
<div class="field"> <BaseSelect label="Select a time" :options="times" v-model="event.time" class="field"/>
<label>Select a time</label>
<select v-model="event.time">
<option v-for="time in times" :key="time">{{ time }}</option>
</select>
</div>
<input type="submit" class="button -fill-gradient" value="Submit"/> <!-- <input type="submit" class="button -fill-gradient" value="Submit"> -->
<BaseButton type="submit" buttonClass="-fill-gradient">Submit</BaseButton>
</form> </form>
</div> </div>
</template> </template>
@ -46,6 +42,7 @@
<script> <script>
import Datepicker from 'vuejs-datepicker' import Datepicker from 'vuejs-datepicker'
import NProgress from 'nprogress'
export default { export default {
components: { components: {
@ -64,6 +61,7 @@ export default {
}, },
methods: { methods: {
createEvent() { createEvent() {
NProgress.start()
this.$store this.$store
.dispatch('event/createEvent', this.event) .dispatch('event/createEvent', this.event)
.then(() => { .then(() => {
@ -73,7 +71,9 @@ export default {
}) })
this.event = this.createFreshEventObject() this.event = this.createFreshEventObject()
}) })
.catch(() => {}) .catch(() => {
NProgress.done()
})
}, },
createFreshEventObject() { createFreshEventObject() {
const user = this.$store.state.user.user const user = this.$store.state.user.user

@ -3,38 +3,64 @@
<h1>Events for {{ user.user.name }}</h1> <h1>Events for {{ user.user.name }}</h1>
<EventCard v-for="event in event.events" :key="event.id" :event="event"/> <EventCard v-for="event in event.events" :key="event.id" :event="event"/>
<template v-if="page != 1"> <template v-if="page != 1">
<router-link :to="{ name: 'event-list', query: { page: page - 1 } }" rel="prev"> <router-link :to="{ name: 'event-list', query: { page: page - 1 } }" rel="prev">Prev Page</router-link>
Prev Page</router-link> <template v-if="hasNextPage">|</template>
<template v-if="hasNextPage"> | </template>
</template> </template>
<router-link v-if="hasNextPage" :to="{ name: 'event-list', query: { page: page + 1 } }" rel="next"> <router-link
Next Page</router-link> v-if="hasNextPage"
:to="{ name: 'event-list', query: { page: page + 1 } }"
rel="next"
>Next Page</router-link>
</div> </div>
</template> </template>
<script> <script>
import EventCard from '@/components/EventCard.vue' import EventCard from '@/components/EventCard.vue'
import { mapState } from 'vuex' import { mapState } from 'vuex'
import store from '@/store/store'
function getPageEvents(routeTo, next) {
const currentPage = parseInt(routeTo.query.page) || 1
store.dispatch('event/fetchEvents', { page: currentPage }).then(() => {
routeTo.params.page = currentPage
next()
})
}
export default { export default {
props: {
page: {
type: Number,
required: true
}
},
components: { components: {
EventCard EventCard
}, },
created() { beforeRouteEnter(routeTo, routeFrom, next) {
this.perPage = 3 // Setting perPage here and not in data means it won't be reactive. getPageEvents(routeTo, next)
// We don't need it to be reactive, and this way our component has access to it.
this.$store.dispatch('event/fetchEvents', {
perPage: this.perPage,
page: this.page
})
}, },
beforeRouteUpdate(routeTo, routeFrom, next) {
getPageEvents(routeTo, next)
},
// created() {
// // This was moved to into the State store so that we had access to it in our route gaurd where we moved the vuex store action call.
// this.perPage = 3 // Setting perPage here and not in data means it won't be reactive.
// // We don't need it to be reactive, and this way our component has access to it.
// // Moved into a route gaurd
// this.$store.dispatch('event/fetchEvents', {
// perPage: this.perPage,
// page: this.page
// })
// },
computed: { computed: {
page() { // removed when we went to a route gaurd pattern
return parseInt(this.$route.query.page) || 1 // page() {
}, // return parseInt(this.$route.query.page) || 1
// },
hasNextPage() { hasNextPage() {
return this.event.eventsTotal > this.page * this.perPage return this.event.eventsTotal > this.page * this.event.perPage
}, },
...mapState(['event', 'user']) ...mapState(['event', 'user'])
} }

@ -0,0 +1,14 @@
<template>
<div>
<h1>Uh oh</h1>
<h3>It looks like you could be experiencing some network issues. Please click the back button and try again.</h3>
<router-link :to="{name: 'event-list'}">Back to the home page</router-link>
</div>
</template>
<script>
export default {}
</script>
<style scoped>
</style>

@ -0,0 +1,21 @@
<template>
<div>
<h1>Ooops</h1>
<h3>The {{ resource }} you are looking for is not here.</h3>
<router-link :to="{name: 'event-list'}">Back to the home page</router-link>
</div>
</template>
<script>
export default {
props: {
resource: {
type: String,
required: true
}
}
}
</script>
<style scoped>
</style>
Loading…
Cancel
Save