Last active
September 7, 2019 15:52
-
-
Save curtisbelt/6c0955576e3449dd74508550f864d890 to your computer and use it in GitHub Desktop.
Sample Vue Components
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
The full project features a page builder on the backend, with various "Flexible Components" | |
the user can select from to build their page. Each of these corresponds to a Vue compoennt | |
on the frontend. | |
There are two ways to use the component: | |
- Manually use a specific Flex component | |
- Automatically load all Flex components (when used on page that supports it) | |
Single usage: | |
``` | |
<AppFlex | |
component="FlexFormSendMessage" | |
:settings="{email: $getPageData('email')}" | |
/> | |
``` | |
Automatic usage -- no props needed, AppFlex does the rest | |
``` | |
<AppFlex /> | |
``` | |
Instead of importing all of the "Flex" vue components on a page, even if most of them may | |
not be used on that page - this AppFlex component will dynamically load them. | |
The parent component's data is $getPageData is made available here via `inject`, which is | |
made possible by the `provide` on the parent component. | |
``` | |
async asyncData(ctx) { | |
return { | |
pageData: await ctx.app.$wp.loadPageData('posts') | |
}; | |
}, | |
provide() { | |
return { | |
$getPageData: this.$getPageData | |
}; | |
}, | |
methods: { | |
$getPageData(path, fallback = null) { | |
return this.$get(this.pageData, path, fallback); | |
} | |
} | |
``` | |
*/ | |
export default { | |
name: 'AppFlex', | |
inject: ['$getPageData'], | |
props: { | |
component: { | |
type: String, | |
default: '' | |
}, | |
settings: { | |
type: Object, | |
default: () => {} | |
} | |
}, | |
inheritAttrs: false, | |
render(h) { | |
if (this.component) { | |
const children = Object.keys(this.$slots).map(slot => | |
h('template', { slot }, this.$slots[slot]) | |
); | |
const loadComponent = this.loadComponents; | |
return h('keep-alive', [ | |
h( | |
loadComponent.component, | |
{ | |
props: { | |
settings: this.$get(loadComponent, 'settings', {}) | |
}, | |
attrs: this.$attrs, | |
on: this.$listeners, | |
scopedSlots: this.$scopedSlots | |
}, | |
children | |
) | |
]); | |
} else if (this.$getPageData('components')) { | |
return h('keep-alive', [ | |
h( | |
'div', | |
{ class: 'AppFlex' }, | |
this.loadComponents.map(function(loaded) { | |
return h(loaded.component, { | |
props: { | |
settings: loaded.settings | |
} | |
}); | |
}) | |
) | |
]); | |
} | |
}, | |
computed: { | |
loadComponents() { | |
if (this.component) { | |
return { | |
component: () => import(`~/components/shared/Flex/${this.component}`), | |
settings: this.settings | |
}; | |
} | |
return this.$getPageData('components', []).map(component => { | |
return { | |
component: () => import(`~/components/shared/Flex/${component._is}`), | |
settings: component | |
}; | |
}); | |
} | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
This component is a wrapper of <NuxtLink> (which itself is a wrapper of Vue Router's <RouterLink>). | |
This wrapper adds some additional features and utility such as: | |
- Automatically downgrades to native elements when neccessary | |
- if no URL given use <span> | |
- if external URL, use normal <a> tag | |
- Handle internationalization | |
- Dynamically add language prefix to URLs | |
- Handle default behavior of /en, where prefix should be striped by default | |
*/ | |
import qs from 'qs'; | |
import get from 'lodash/get'; | |
export default { | |
functional: true, | |
name: 'AppLink', | |
props: { | |
to: { | |
type: [String, Object], | |
required: false | |
}, | |
target: { | |
type: String, | |
required: false | |
}, | |
rel: { | |
type: String, | |
required: false | |
}, | |
query: { | |
type: Object, | |
required: false | |
}, | |
tag: { | |
type: String, | |
default: '' | |
} | |
}, | |
render(createElement, context) { | |
const linkAttributes = (function() { | |
let url = get(context.props, 'to', null); | |
if (typeof url === 'object') { | |
url = get(context.props, 'to.path', ''); | |
} | |
/** | |
* * Handle empty URLs | |
*/ | |
if (url === null || url.length == 0) { | |
return { | |
is: 'span', | |
to: '' | |
}; | |
} | |
/** | |
* Fix absolute URLs which really should have been relative | |
* Example: "http://stamford.localhost/home" -> "/home" | |
*/ | |
const indexOfHost = url.indexOf('//' + context.parent.$store.state.host); | |
if (indexOfHost !== -1) { | |
url = url.substring(indexOfHost + 2 + context.parent.$store.state.host.length); | |
} | |
/** | |
* * Handle relative URLs | |
*/ | |
const matchRelativeUrl = url.match(/^\/.*/); | |
const isRelativeUrl = matchRelativeUrl === null ? false : true; | |
if (isRelativeUrl) { | |
const matchLangPrefix = url.match(/(?:^\/)([a-z]{2})(?:\/|$)/); // Match urls like "/de", "/de/about" | |
const hasLangPrefix = matchLangPrefix === null ? false : true; | |
// * If lang prefix isn't specified, then add the prefix we are already routing with (if any) | |
if (!hasLangPrefix) { | |
const currentlyRoutedLang = get(context.parent.$route, 'params.lang', ''); | |
if (currentlyRoutedLang.length > 0) { | |
url = '/' + currentlyRoutedLang + url; | |
} | |
} | |
// * If lang prefix exists but it's English, then force no prefix. | |
// ! Note that switching TO English will require explicitly setting "/en/", otherwise it's indistinguishable from regular relative URLs that need to be dynamically prefixed. | |
if (hasLangPrefix && matchLangPrefix[1] === 'en') { | |
url = url.substring(3); | |
// Edge-case: If URL is "/en", url would be empty, which will keep us on the same page instead of taking us to '/'. | |
if (url.length == 0) { | |
url = '/'; | |
} | |
} | |
// Stringify query prop into URL params | |
if (context.props.query) { | |
url = url + '?' + qs.stringify(context.props.query); | |
} | |
/** | |
* If `to` prop is an object, it's because it used the `{path: '', query:{}}` syntax. | |
* Checking for that here and restoring the query object if needed. | |
* * Only used in the return for NuxtLink | |
*/ | |
let to; | |
if (typeof get(context.props, 'to') === 'object') { | |
to = { | |
path: url, | |
query: JSON.parse(JSON.stringify(get(context.props, 'to.query'))) | |
}; | |
} else { | |
to = url; | |
} | |
// 🎉 | |
return { | |
is: 'NuxtLink', | |
to: to, | |
tag: context.props.tag || 'a', | |
target: context.props.target, | |
rel: context.props.rel, | |
...(url == '/' && { exact: true }) | |
}; | |
/** | |
* * Handle absolute URLs | |
*/ | |
} else { | |
return { | |
is: 'a', | |
href: url, | |
target: context.props.target || '_blank', | |
rel: context.props.rel || 'noopener noreferrer' | |
}; | |
} | |
})(); | |
return createElement( | |
linkAttributes.is, | |
{ | |
...context.data, | |
props: linkAttributes, | |
attrs: linkAttributes | |
}, | |
context.slots().default | |
); | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<div | |
id="FlexFormSendMessage" | |
class="FlexFormSendMessage bg-gray-100 border-gray-300 border-t relative" | |
> | |
<form | |
class="px-6 py-6 md:px-4 max-w-2xl m-auto" | |
@submit.prevent="submit" | |
> | |
<div class="flex items-center my-6 text-3xl lg:text-4xl font-serif"> | |
{{ $get(settings,'title','Send a Message') + ' ' }} | |
<span | |
v-if="$get(settings, 'email', '')" | |
class="text-lg text-brand-blue-200 mx-4 font-sans font-hairline tracking-tight" | |
>{{ $get(settings, 'email', '') }} | |
</span> | |
</div> | |
<div class="-mx-4 -my-2 flex flex-wrap justify-between w-full"> | |
<div class="w-1/2 px-4 py-2"> | |
<label | |
for="first_name" | |
class="block my-2 text-lg" | |
> | |
First Name * | |
</label> | |
<FormText | |
id="first_name" | |
v-model="body.first_name" | |
:required="true" | |
:sync-route-query="false" | |
class="w-full" | |
type="text" | |
name="first_name" | |
/> | |
</div> | |
<div class="w-1/2 px-4 py-2"> | |
<label | |
for="last_name" | |
class="block my-2 text-lg" | |
> | |
Last Name * | |
</label> | |
<FormText | |
id="last_name" | |
v-model="body.last_name" | |
:required="true" | |
:sync-route-query="false" | |
class="w-full" | |
type="text" | |
name="last_name" | |
/> | |
</div> | |
<div class="w-1/2 px-4 py-2"> | |
<label | |
for="email" | |
class="block my-2 text-lg" | |
> | |
Email * | |
</label> | |
<FormText | |
id="email" | |
v-model="body.email" | |
:required="true" | |
:sync-route-query="false" | |
class="w-full" | |
type="email" | |
name="email" | |
/> | |
</div> | |
<div class="w-1/2 px-4 py-2"> | |
<label | |
for="phone" | |
class="block my-2 text-lg" | |
> | |
Phone Number | |
</label> | |
<FormText | |
id="phone" | |
v-model="body.phone_number" | |
v-mask="'(###) ###-####'" | |
:sync-route-query="false" | |
class="w-full" | |
name="phone" | |
placeholder="(000) 000-0000" | |
type="tel" | |
/> | |
</div> | |
<div class="w-full px-4 py-2"> | |
<label | |
for="name" | |
class="block my-2 text-lg" | |
> | |
Your Message | |
</label> | |
<textarea | |
id="message" | |
ref="message" | |
v-model="body.message" | |
:required="true" | |
name="message" | |
class="pl-4 pt-2 h-12 w-full border border-gray-300 text-xs leading-loose" | |
/> | |
</div> | |
</div> | |
<div class="text-center"> | |
<button | |
class="bg-brand-blue-300 text-white px-8 py-3 my-8 mx-auto text-center text-xs tracking-wide uppercase" | |
> | |
{{ $get(settings,'submit_label','Send') }} | |
</button> | |
</div> | |
</form> | |
<div | |
v-if="showSuccess" | |
class="SuccessMessage absolute inset-0 flex justify-center items-center" | |
> | |
<span class="p-4 text-lg bg-brand-blue-400 text-white">message sent</span> | |
</div> | |
</div> | |
</template> | |
<script> | |
import FormText from '~/components/shared/Form/FormText'; | |
import { mask } from 'vue-the-mask'; | |
export default { | |
name: 'FlexFormSendMessage', | |
components: { | |
FormText | |
}, | |
directives: { mask }, | |
props: { | |
settings: { | |
type: Object, | |
default: () => {} | |
} | |
}, | |
inject: ['$getPageData'], | |
data() { | |
return { | |
showSuccess: false, | |
body: { | |
first_name: null, | |
last_name: null, | |
email: null, | |
phone_number: null, | |
message: null | |
} | |
}; | |
}, | |
methods: { | |
async submit() { | |
let contact, | |
contact_id, | |
property_id, | |
user_id = null; | |
if (this.$myTribusEmbed.isAuthenticated()) { | |
contact = this.$myTribusEmbed.getContact(); | |
contact_id = contact.id; | |
} | |
if (this.$getPageData('listing')) { | |
property_id = this.$getPageData('id'); | |
} | |
if (this.$getPageData('display_name')) { | |
user_id = this.$getPageData('id'); | |
} | |
let response = await this.$displet.postMessage({ | |
id: null, | |
contact_id, | |
property_id, | |
user_id, | |
requested_showing_at: null, | |
body: JSON.stringify(this.body) | |
}); | |
if (response) { | |
this.showSuccess = true; | |
window.setTimeout(() => { | |
this.showSuccess = false; | |
}, 2000); | |
} | |
} | |
} | |
}; | |
</script> | |
<style lang='pcss' scoped> | |
textarea { | |
color: #d5d5d5; | |
height: 280px; | |
} | |
input::placeholder { | |
color: #d5d5d5; | |
} | |
</style> | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<div class="FlexMap"> | |
<h3 class="font-serif text-brand-blue-400 text-4xl font-hairline text-center my-4"> | |
{{ $get(settings, 'title', '') }} | |
</h3> | |
<AppContainer | |
class="my-4" | |
v-html="$get(settings, 'text', '')" | |
/> | |
<div class="w-full flex justify-end"> | |
<a | |
class="ttext-sm uppercase font-hairline text-brand-blue-400 p-2" | |
target="blank" | |
:href="'https://www.google.com/maps/dir/?api=1&destination=' + encodeURIComponent($get(settings, 'map.address'))" | |
> | |
get directions | |
</a> | |
</div> | |
<div | |
class="relative z-20" | |
style="height: 397px" | |
> | |
<ClientOnly> | |
<l-map | |
ref="MyMap" | |
:center="center" | |
:zoom="10" | |
> | |
<l-tile-layer url="https://{s}.tile.osm.org/{z}/{x}/{y}.png" /> | |
<l-marker | |
:lat-lng="center" | |
/> | |
</l-map> | |
</ClientOnly> | |
</div> | |
</div> | |
</template> | |
<script> | |
export default { | |
name: 'FlexMap', | |
props: { | |
settings: { | |
type: Object, | |
default: () => {} | |
} | |
}, | |
data() { | |
return { | |
center: [this.$get(this.settings, 'map.lat', 0), this.$get(this.settings, 'map.lng', 0)], | |
community: null | |
}; | |
}, | |
mounted() { | |
this.$wp | |
.posts({ | |
id: this.settings.community_post_id | |
}) | |
.then(data => { | |
this.community = this.$get(data, 'results[0]'); | |
}); | |
this.$nextTick(() => { | |
if (this.$refs.MyMap) { | |
let map = this.$refs.MyMap.mapObject; | |
map.scrollWheelZoom.disable(); | |
map.on('focus', function() { | |
map.scrollWheelZoom.enable(); | |
}); | |
map.on('blur', function() { | |
map.scrollWheelZoom.disable(); | |
}); | |
} | |
}); | |
} | |
}; | |
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<div class="UiPagination flex"> | |
<div | |
class="hidden md:block mr-1 relative border border-gray-300 text-xs bg-white px-2 py-2 cursor-pointer select-none" | |
:class="{'opacity-50 cursor-not-allowed': currentPage <= 1}" | |
@click="goToPage(currentPage-1)" | |
> | |
<AppIcon | |
icon="chevron-left" | |
class="h-full text-3xs pointer-events-none" | |
/> | |
</div> | |
<FormSelect | |
:choices="pageChoices" | |
name="page" | |
:value="1" | |
:disabled="totalPages<=1" | |
:sync-route-query="true" | |
/> | |
<div | |
class="hidden md:block ml-1 relative border border-gray-300 text-xs bg-white px-2 py-2 cursor-pointer select-none" | |
:class="{'opacity-50 cursor-not-allowed': currentPage >= totalPages}" | |
@click="goToPage(currentPage+1)" | |
> | |
<AppIcon | |
icon="chevron-right" | |
class="h-full text-3xs pointer-events-none" | |
/> | |
</div> | |
</div> | |
</template> | |
<script> | |
import FormSelect from '~/components/shared/Form/FormSelect'; | |
export default { | |
name: 'UiPagination', | |
components: { | |
FormSelect | |
}, | |
inject: ['$getPageData'], | |
computed: { | |
currentPage() { | |
return parseInt(this.$route.query.page ? this.$route.query.page : 1); | |
}, | |
totalPages() { | |
let totalLimit = this.$getPageData('total', 0) > 9500 ? 9500 : this.$getPageData('total', 0); | |
return Math.ceil(totalLimit / this.$getPageData('__params.per_page')); | |
}, | |
pageChoices() { | |
let choices = []; | |
for (let i = 1; i <= this.totalPages; i++) { | |
choices.push({ | |
label: i.toLocaleString(), | |
value: i | |
}); | |
} | |
return choices; | |
} | |
}, | |
methods: { | |
goToSelectedPage(event) { | |
this.goToPage(event.target.value); | |
}, | |
goToPage(pageNumber, event) { | |
if (pageNumber > this.totalPages || pageNumber < 1) { | |
return; | |
} | |
if (pageNumber == 1) { | |
pageNumber = undefined; | |
} | |
this.$router.push({ | |
query: { ...this.$route.query, page: pageNumber } | |
}); | |
} | |
} | |
}; | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment