aboutsummaryrefslogtreecommitdiff
path: root/src/hocs
diff options
context:
space:
mode:
Diffstat (limited to 'src/hocs')
-rw-r--r--src/hocs/with_list/with_list.js40
-rw-r--r--src/hocs/with_list/with_list.scss6
-rw-r--r--src/hocs/with_load_more/with_load_more.js94
-rw-r--r--src/hocs/with_load_more/with_load_more.scss10
-rw-r--r--src/hocs/with_subscription/with_subscription.js84
-rw-r--r--src/hocs/with_subscription/with_subscription.scss10
6 files changed, 244 insertions, 0 deletions
diff --git a/src/hocs/with_list/with_list.js b/src/hocs/with_list/with_list.js
new file mode 100644
index 00000000..896f8fc8
--- /dev/null
+++ b/src/hocs/with_list/with_list.js
@@ -0,0 +1,40 @@
+import Vue from 'vue'
+import map from 'lodash/map'
+import isEmpty from 'lodash/isEmpty'
+import './with_list.scss'
+
+const defaultEntryPropsGetter = entry => ({ entry })
+const defaultKeyGetter = entry => entry.id
+
+const withList = ({
+ getEntryProps = defaultEntryPropsGetter, // function to accept entry and index values and return props to be passed into the item component
+ getKey = defaultKeyGetter // funciton to accept entry and index values and return key prop value
+}) => (ItemComponent) => (
+ Vue.component('withList', {
+ props: [
+ 'entries', // array of entry
+ 'entryProps', // additional props to be passed into each entry
+ 'entryListeners' // additional event listeners to be passed into each entry
+ ],
+ render (createElement) {
+ return (
+ <div class="with-list">
+ {map(this.entries, (entry, index) => {
+ const props = {
+ key: getKey(entry, index),
+ props: {
+ ...this.$props.entryProps,
+ ...getEntryProps(entry, index)
+ },
+ on: this.$props.entryListeners
+ }
+ return <ItemComponent {...props} />
+ })}
+ {isEmpty(this.entries) && this.$slots.empty && <div class="with-list-empty-content faint">{this.$slots.empty}</div>}
+ </div>
+ )
+ }
+ })
+)
+
+export default withList
diff --git a/src/hocs/with_list/with_list.scss b/src/hocs/with_list/with_list.scss
new file mode 100644
index 00000000..c6e13d5b
--- /dev/null
+++ b/src/hocs/with_list/with_list.scss
@@ -0,0 +1,6 @@
+.with-list {
+ &-empty-content {
+ text-align: center;
+ padding: 10px;
+ }
+} \ No newline at end of file
diff --git a/src/hocs/with_load_more/with_load_more.js b/src/hocs/with_load_more/with_load_more.js
new file mode 100644
index 00000000..74979b87
--- /dev/null
+++ b/src/hocs/with_load_more/with_load_more.js
@@ -0,0 +1,94 @@
+import Vue from 'vue'
+import isEmpty from 'lodash/isEmpty'
+import { getComponentProps } from '../../services/component_utils/component_utils'
+import './with_load_more.scss'
+
+const withLoadMore = ({
+ fetch, // function to fetch entries and return a promise
+ select, // function to select data from store
+ destroy, // function called at "destroyed" lifecycle
+ childPropName = 'entries', // name of the prop to be passed into the wrapped component
+ additionalPropNames = [] // additional prop name list of the wrapper component
+}) => (WrappedComponent) => {
+ const originalProps = Object.keys(getComponentProps(WrappedComponent))
+ const props = originalProps.filter(v => v !== childPropName).concat(additionalPropNames)
+
+ return Vue.component('withLoadMore', {
+ render (createElement) {
+ const props = {
+ props: {
+ ...this.$props,
+ [childPropName]: this.entries
+ },
+ on: this.$listeners,
+ scopedSlots: this.$scopedSlots
+ }
+ const children = Object.entries(this.$slots).map(([key, value]) => createElement('template', { slot: key }, value))
+ return (
+ <div class="with-load-more">
+ <WrappedComponent {...props}>
+ {children}
+ </WrappedComponent>
+ <div class="with-load-more-footer">
+ {this.error && <a onClick={this.fetchEntries} class="alert error">{this.$t('general.generic_error')}</a>}
+ {!this.error && this.loading && <i class="icon-spin3 animate-spin"/>}
+ {!this.error && !this.loading && !this.bottomedOut && <a onClick={this.fetchEntries}>{this.$t('general.more')}</a>}
+ </div>
+ </div>
+ )
+ },
+ props,
+ data () {
+ return {
+ loading: false,
+ bottomedOut: false,
+ error: false
+ }
+ },
+ computed: {
+ entries () {
+ return select(this.$props, this.$store) || []
+ }
+ },
+ created () {
+ window.addEventListener('scroll', this.scrollLoad)
+ if (this.entries.length === 0) {
+ this.fetchEntries()
+ }
+ },
+ destroyed () {
+ window.removeEventListener('scroll', this.scrollLoad)
+ destroy && destroy(this.$props, this.$store)
+ },
+ methods: {
+ fetchEntries () {
+ if (!this.loading) {
+ this.loading = true
+ this.error = false
+ fetch(this.$props, this.$store)
+ .then((newEntries) => {
+ this.loading = false
+ this.bottomedOut = isEmpty(newEntries)
+ })
+ .catch(() => {
+ this.loading = false
+ this.error = true
+ })
+ }
+ },
+ scrollLoad (e) {
+ const bodyBRect = document.body.getBoundingClientRect()
+ const height = Math.max(bodyBRect.height, -(bodyBRect.y))
+ if (this.loading === false &&
+ this.bottomedOut === false &&
+ this.$el.offsetHeight > 0 &&
+ (window.innerHeight + window.pageYOffset) >= (height - 750)
+ ) {
+ this.fetchEntries()
+ }
+ }
+ }
+ })
+}
+
+export default withLoadMore
diff --git a/src/hocs/with_load_more/with_load_more.scss b/src/hocs/with_load_more/with_load_more.scss
new file mode 100644
index 00000000..1a0a9c40
--- /dev/null
+++ b/src/hocs/with_load_more/with_load_more.scss
@@ -0,0 +1,10 @@
+.with-load-more {
+ &-footer {
+ padding: 10px;
+ text-align: center;
+
+ .error {
+ font-size: 14px;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/hocs/with_subscription/with_subscription.js b/src/hocs/with_subscription/with_subscription.js
new file mode 100644
index 00000000..679409cf
--- /dev/null
+++ b/src/hocs/with_subscription/with_subscription.js
@@ -0,0 +1,84 @@
+import Vue from 'vue'
+import isEmpty from 'lodash/isEmpty'
+import { getComponentProps } from '../../services/component_utils/component_utils'
+import './with_subscription.scss'
+
+const withSubscription = ({
+ fetch, // function to fetch entries and return a promise
+ select, // function to select data from store
+ childPropName = 'content', // name of the prop to be passed into the wrapped component
+ additionalPropNames = [] // additional prop name list of the wrapper component
+}) => (WrappedComponent) => {
+ const originalProps = Object.keys(getComponentProps(WrappedComponent))
+ const props = originalProps.filter(v => v !== childPropName).concat(additionalPropNames)
+
+ return Vue.component('withSubscription', {
+ props: [
+ ...props,
+ 'refresh' // boolean saying to force-fetch data whenever created
+ ],
+ render (createElement) {
+ if (!this.error && !this.loading) {
+ const props = {
+ props: {
+ ...this.$props,
+ [childPropName]: this.fetchedData
+ },
+ on: this.$listeners,
+ scopedSlots: this.$scopedSlots
+ }
+ const children = Object.entries(this.$slots).map(([key, value]) => createElement('template', { slot: key }, value))
+ return (
+ <div class="with-subscription">
+ <WrappedComponent {...props}>
+ {children}
+ </WrappedComponent>
+ </div>
+ )
+ } else {
+ return (
+ <div class="with-subscription-loading">
+ {this.error
+ ? <a onClick={this.fetchData} class="alert error">{this.$t('general.generic_error')}</a>
+ : <i class="icon-spin3 animate-spin"/>
+ }
+ </div>
+ )
+ }
+ },
+ data () {
+ return {
+ loading: false,
+ error: false
+ }
+ },
+ computed: {
+ fetchedData () {
+ return select(this.$props, this.$store)
+ }
+ },
+ created () {
+ if (this.refresh || isEmpty(this.fetchedData)) {
+ this.fetchData()
+ }
+ },
+ methods: {
+ fetchData () {
+ if (!this.loading) {
+ this.loading = true
+ this.error = false
+ fetch(this.$props, this.$store)
+ .then(() => {
+ this.loading = false
+ })
+ .catch(() => {
+ this.error = true
+ this.loading = false
+ })
+ }
+ }
+ }
+ })
+}
+
+export default withSubscription
diff --git a/src/hocs/with_subscription/with_subscription.scss b/src/hocs/with_subscription/with_subscription.scss
new file mode 100644
index 00000000..52c7d94c
--- /dev/null
+++ b/src/hocs/with_subscription/with_subscription.scss
@@ -0,0 +1,10 @@
+.with-subscription {
+ &-loading {
+ padding: 10px;
+ text-align: center;
+
+ .error {
+ font-size: 14px;
+ }
+ }
+} \ No newline at end of file