@bcrs-shared-components/base-address 2.0.11 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,215 @@
1
+ /* eslint-disable */
2
+ import { computed, reactive, ref, Ref } from 'vue-demi'
3
+ import { uniqueId } from 'lodash'
4
+
5
+ import { AddressIF, SchemaIF } from '@bcrs-shared-components/interfaces'
6
+
7
+ export function useAddress (address: Ref<AddressIF>, schema: SchemaIF) {
8
+ const addressLocal = address
9
+ /** The Address Country, to simplify the template and so we can watch it directly. */
10
+ const country = computed((): string => {
11
+ return addressLocal.value.country
12
+ })
13
+ const schemaLocal = ref(schema)
14
+ const isSchemaRequired = (prop: string): boolean => {
15
+ if (!schemaLocal || !schemaLocal.value[prop]) return false
16
+
17
+ // check for any rule that does not allow an empty string
18
+ for (let index in schemaLocal.value[prop]) {
19
+ if (schemaLocal.value[prop][index]('') !== true) return true
20
+ }
21
+ return false
22
+ }
23
+ const labels = {
24
+ /** The Street Address Additional label with 'optional' as needed. */
25
+ streetAdditionalLabel: computed((): string => {
26
+ return 'Address Line 2' + (isSchemaRequired('streetAdditional') ? '' : ' (Optional)')
27
+ }),
28
+ /** The Street Address label with 'optional' as needed. */
29
+ streetLabel: computed((): string => {
30
+ return 'Address Line 1' + (isSchemaRequired('street') ? '' : ' (Optional)')
31
+ }),
32
+ /** The Address City label with 'optional' as needed. */
33
+ cityLabel: computed((): string => {
34
+ return 'City' + (isSchemaRequired('city') ? '' : ' (Optional)')
35
+ }),
36
+ /** The Address Region label with 'optional' as needed. */
37
+ regionLabel: computed((): string => {
38
+ let label: string
39
+ let required = isSchemaRequired('region')
40
+
41
+ // NB: make region required for Canada and USA
42
+ if (addressLocal.value.country === 'CA') {
43
+ label = 'Province'
44
+ required = true
45
+ } else if (addressLocal.value.country === 'US') {
46
+ label = 'State'
47
+ required = true
48
+ } else {
49
+ label = 'Region'
50
+ }
51
+ return label + (required ? '' : ' (Optional)')
52
+ }),
53
+ /** The Postal Code label with 'optional' as needed. */
54
+ postalCodeLabel: computed((): string => {
55
+ let label: string
56
+ if (addressLocal.value.country === 'US') {
57
+ label = 'Zip Code'
58
+ } else {
59
+ label = 'Postal Code'
60
+ }
61
+ return label + (isSchemaRequired('postalCode') ? '' : ' (Optional)')
62
+ }),
63
+ /** The Address Country label with 'optional' as needed. */
64
+ countryLabel: computed((): string => {
65
+ return 'Country' + (isSchemaRequired('country') ? '' : ' (Optional)')
66
+ }),
67
+ /** The Delivery Instructions label with 'optional' as needed. */
68
+ deliveryInstructionsLabel: computed((): string => {
69
+ return 'Delivery Instructions' + (isSchemaRequired('deliveryInstructions') ? '' : ' (Optional)')
70
+ })
71
+ }
72
+ return {
73
+ addressLocal,
74
+ country,
75
+ schemaLocal,
76
+ isSchemaRequired,
77
+ labels
78
+ }
79
+ }
80
+
81
+ export function useAddressComplete (addressLocal: Ref<AddressIF>) {
82
+ const combineLines = (line1: string, line2: string) => {
83
+ if (!line1) return line2
84
+ if (!line2) return line1
85
+ return line1 + '\n' + line2
86
+ }
87
+ /**
88
+ * Callback to update the address data after the user chooses a suggested address.
89
+ * @param address the data object returned by the AddressComplete Retrieve API
90
+ */
91
+ const addressCompletePopulate = (addressComplete: object): void => {
92
+ addressLocal.value.streetAddress = addressComplete['Line1'] || 'N/A'
93
+ // Combine extra address lines into Street Address Additional field.
94
+ addressLocal.value.streetAddressAdditional = combineLines(
95
+ combineLines(addressComplete['Line2'], addressComplete['Line3']),
96
+ combineLines(addressComplete['Line4'], addressComplete['Line5'])
97
+ )
98
+ addressLocal.value.city = addressComplete['City']
99
+ if (useCountryRegions(addressComplete['CountryIso2'])) {
100
+ // In this case, v-select will map known province code to province name
101
+ // or v-select will be blank and user will have to select a known item.
102
+ addressLocal.value.region = addressComplete['ProvinceCode']
103
+ } else {
104
+ // In this case, v-text-input will allow manual entry but province info is probably too long
105
+ // so set region to null and add province name to the Street Address Additional field.
106
+ // If length is excessive, user will have to fix it.
107
+ addressLocal.value.region = ''
108
+ addressLocal.value.streetAdditional = combineLines(
109
+ addressLocal.value.streetAdditional, addressComplete['ProvinceName']
110
+ )
111
+ }
112
+ addressLocal.value.postalCode = addressComplete['PostalCode']
113
+ addressLocal.value.country = addressComplete['CountryIso2']
114
+ }
115
+ const uniqueIds = reactive({
116
+ /** A unique id for this instance of this component. */
117
+ uniqueId: uniqueId(),
118
+ /** A unique id for the Street Address input. */
119
+ streetId: computed((): string => {
120
+ return `street-address-${uniqueIds.uniqueId}`
121
+ }),
122
+ /** A unique id for the Address Country input. */
123
+ countryId: computed((): string => {
124
+ return `address-country-${uniqueIds.uniqueId}`
125
+ })
126
+ })
127
+ /**
128
+ * Creates the AddressComplete object for this instance of the component.
129
+ * @param pca the Postal Code Anywhere object provided by AddressComplete
130
+ * @param key the key for the Canada Post account that is to be charged for lookups
131
+ * @returns an object that is a pca.Address instance
132
+ */
133
+ const createAddressComplete = (pca: any, key: string): object => {
134
+ // Set up the two fields that AddressComplete will use for input.
135
+ // Ref: https://www.canadapost.ca/pca/support/guides/advanced
136
+ // Note: Use special field for country, which user can't click, and which AC will overwrite
137
+ // but that we don't care about.
138
+ const fields = [
139
+ { element: uniqueIds.streetId, field: 'Line1', mode: pca.fieldMode.SEARCH },
140
+ { element: uniqueIds.countryId, field: 'CountryName', mode: pca.fieldMode.COUNTRY }
141
+ ]
142
+ const options = { key }
143
+
144
+ const addressComplete = new pca.Address(fields, options)
145
+
146
+ // The documentation contains sample load/populate callback code that doesn't work, but this will. The side effect
147
+ // is that it breaks the autofill functionality provided by the library, but we really don't want the library
148
+ // altering the DOM because Vue is already doing so, and the two don't play well together.
149
+ addressComplete.listen('populate', addressCompletePopulate)
150
+
151
+ return addressComplete
152
+ }
153
+ /** Enables AddressComplete for this instance of the address. */
154
+ const enableAddressComplete = (): void => {
155
+ // If you want to use this component with the Canada Post AddressComplete service:
156
+ // 1. The AddressComplete JavaScript script (and stylesheet) must be loaded.
157
+ // 2. Your AddressComplete account key must be defined.
158
+ const pca = window['pca']
159
+ const key = window['addressCompleteKey']
160
+ if (!pca || !key) {
161
+ // eslint-disable-next-line no-console
162
+ console.log('AddressComplete not initialized due to missing script and/or key')
163
+ return
164
+ }
165
+
166
+ // Destroy the old object if it exists, and create a new one.
167
+ if (window['currentAddressComplete']) {
168
+ window['currentAddressComplete'].destroy()
169
+ }
170
+ window['currentAddressComplete'] = createAddressComplete(pca, key)
171
+ }
172
+ return {
173
+ addressCompletePopulate,
174
+ createAddressComplete,
175
+ enableAddressComplete,
176
+ uniqueIds
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Determines whether to use a country's known regions (ie, provinces/states).
182
+ * @param code the short code of the country
183
+ * @returns whether to use v-select (true) or v-text-field (false) for input
184
+ */
185
+ export function useCountryRegions (code: string): boolean {
186
+ return (code === 'CA' || code === 'US')
187
+ }
188
+
189
+ export function formatAddress (address: AddressIF): AddressIF {
190
+ address.postalCode = address.postalCode?.toUpperCase() || ''
191
+ if (address.country === 'CA') {
192
+ address.postalCode = address.postalCode.replace('-', ' ')
193
+ if (address.postalCode.length > 4 && address.postalCode[3] !== ' ') {
194
+ address.postalCode = address.postalCode.slice(0, 3) + ' ' + address.postalCode.slice(3,)
195
+ }
196
+ }
197
+ return {
198
+ country: address.country?.trim(),
199
+ street: address.street?.trim(),
200
+ streetAdditional: address.streetAdditional?.trim(),
201
+ city: address.city?.trim(),
202
+ region: address.region?.trim(),
203
+ postalCode: address.postalCode?.trim(),
204
+ deliveryInstructions: address.deliveryInstructions?.trim()
205
+ }
206
+ }
207
+
208
+ export function checkAddress (address: AddressIF, schema: SchemaIF): AddressIF {
209
+ for(var addressProperty in schema) {
210
+ if (!address[addressProperty]) {
211
+ address[addressProperty] = ''
212
+ }
213
+ }
214
+ return address
215
+ }
@@ -0,0 +1,78 @@
1
+ import countriesData from 'country-list/data.json'
2
+ import provincesData from 'provinces/provinces.json'
3
+
4
+ window['countries'] = window['countries'] || countriesData.sort((a, b) =>
5
+ (a.name < b.name) ? -1 : (a.name > b.name) ? 1 : 0)
6
+
7
+ window['provinces'] = window['provinces'] || provincesData.sort((a, b) =>
8
+ (a.name < b.name) ? -1 : (a.name > b.name) ? 1 : 0)
9
+
10
+ // global caching to improve performance when called multiple times
11
+ window['countryNameCache'] = {}
12
+ window['countryRegionsCache'] = {}
13
+
14
+ /**
15
+ * Factory that allows VM access to useful country/province data and functions.
16
+ * @link https://www.npmjs.com/package/country-list
17
+ * @lint https://www.npmjs.com/package/provinces
18
+ */
19
+ export function useCountriesProvinces () {
20
+ /**
21
+ * Helper function to return a list of countries.
22
+ * @returns An array of country objects, sorted alphabetically.
23
+ */
24
+ const getCountries = (naOnly = false): Array<object> => {
25
+ let countries = []
26
+ countries.push({ code: 'CA', name: 'Canada' })
27
+ countries.push({ code: 'US', name: 'United States' })
28
+ if (!naOnly) {
29
+ // name is set this way to ensure the divider is there in the search when CA/US are not the only options
30
+ countries.push({ code: '0', name: 'Can.nada. United States .Of.America', divider: true })
31
+ countries = countries.concat(window['countries'])
32
+ }
33
+ return countries
34
+ }
35
+ /**
36
+ * Helper function to return a country's name.
37
+ * @param code The short code of the country.
38
+ * @returns The long name of the country.
39
+ */
40
+ const getCountryName = (code: string): string => {
41
+ if (!code) return null
42
+ if (window['countryNameCache'][code]) return window['countryNameCache'][code]
43
+ const country = window['countries'].find(c => c.code === code)
44
+ const result = country ? country.name : null
45
+ window['countryNameCache'][code] = result
46
+ return result
47
+ }
48
+ /**
49
+ * Helper function to return a country's list of provinces.
50
+ * @param code The short code of the country.
51
+ * @param overrideDefault A flag to bypass manual defaults.
52
+ * @returns An array of province objects, sorted alphabetically.
53
+ */
54
+ const getCountryRegions = (code: string, overrideDefault = false): Array<object> => {
55
+ if (!code) return []
56
+ if (window['countryRegionsCache'][code]) return window['countryRegionsCache'][code]
57
+ let regions = []
58
+ if (code === 'CA' && !overrideDefault) {
59
+ regions.push({ name: 'British Columbia', short: 'BC' })
60
+ // name is set this way to ensure the divider is there in the search when BC is not the only option
61
+ regions.push({ code: '0', name: 'Br.it.is.h.Co.l.u.m.b.ia', divider: true })
62
+ }
63
+ const result = window['provinces']
64
+ .filter(p => p.country === code)
65
+ .map(p => ({
66
+ name: p.english || p.name,
67
+ short: (p.short && p.short.length <= 2) ? p.short : '--'
68
+ }))
69
+ regions = regions.concat(result)
70
+ window['countryRegionsCache'][code] = regions
71
+ return regions
72
+ }
73
+ return {
74
+ getCountries,
75
+ getCountryName,
76
+ getCountryRegions
77
+ }
78
+ }
@@ -0,0 +1,3 @@
1
+ export { formatAddress, useAddress, useAddressComplete, useCountryRegions } from './address-factory'
2
+ export { useCountriesProvinces } from './countries-provinces-factory'
3
+ export { baseRules, spaceRules, useBaseValidations } from './validation-factory'
@@ -0,0 +1,42 @@
1
+ import { ref } from 'vue-demi'
2
+
3
+ import { AddressValidationRules } from '@bcrs-shared-components/enums'
4
+
5
+ /* Sets up form validation functions */
6
+ export function useBaseValidations () {
7
+ /* this variable must be named the same as your ref=___ in your form */
8
+ const addressForm = ref(null)
9
+ const resetValidation = () => {
10
+ addressForm.value?.resetValidation()
11
+ }
12
+ const validate = () => {
13
+ addressForm.value?.validate()
14
+ }
15
+ return { addressForm, resetValidation, validate }
16
+ }
17
+
18
+ /* Rules used in most schemas */
19
+ export const baseRules = {
20
+ [AddressValidationRules.BC]: (v: string) => v === 'BC' || v === 'British Columbia' || 'Address must be in BC',
21
+ [AddressValidationRules.CANADA]: (v: string) => v === 'CA' || 'Address must be in Canada',
22
+ [AddressValidationRules.MAX_LENGTH]: (max: number) => {
23
+ return (v: string) => (v || '').length <= max || `Maximum ${max} characters`
24
+ },
25
+ [AddressValidationRules.MIN_LENGTH]: (min: number) => {
26
+ return (v: string) => (v || '').length >= min || `Minimum ${min} characters`
27
+ },
28
+ [AddressValidationRules.POSTAL_CODE]: (v: string) => (
29
+ /^\s*[ABCEGHJ-NPRSTVXY]\d[ABCEGHJ-NPRSTV-Z][\s-]?\d[ABCEGHJ-NPRSTV-Z]\d\s*$/i.test(v) ||
30
+ 'Must be a valid postal code'
31
+ ),
32
+ [AddressValidationRules.REQUIRED]: (v: string) => v?.length > 0 || 'This field is required',
33
+ [AddressValidationRules.ZIP_CODE]: (v: string) => (
34
+ /^\s*[0-9]{5}([\s-]?[0-9]{4})?\s*$/i.test(v) ||
35
+ 'Must be a valid zip code'
36
+ )
37
+ }
38
+
39
+ /* Array of validation rules used by input elements to prevent extra whitespace. */
40
+ export const spaceRules = [
41
+ (v: string) => !/^\s/g.test(v) || 'Invalid spaces' // leading spaces
42
+ ]
@@ -0,0 +1,118 @@
1
+ /* FUTURE:
2
+ * Delete this if we decide not to move forward with vuelidate
3
+ * Fix it to work otherwise
4
+ */
5
+ import { computed, Ref, reactive } from 'vue-demi'
6
+
7
+ import { AddressIF, SchemaIF } from '@bcrs-shared-components/interfaces'
8
+
9
+ import useVuelidate from '@vuelidate/core'
10
+
11
+ export function useValidations (schema: Ref<SchemaIF>, address: Ref<AddressIF>) {
12
+ const validations = reactive({ addressLocal: schema.value })
13
+ const $v = useVuelidate(validations, reactive({ addressLocal: address.value }))
14
+
15
+ /**
16
+ * Misc Vuetify rules.
17
+ * @param prop The name of the property object to validate.
18
+ * @param key The name of the property key (field) to validate.
19
+ * @returns True if the rule passes, otherwise an error string.
20
+ */
21
+ const vuetifyRules = {
22
+ requiredRule: (prop: string, key: string): boolean | string => {
23
+ return Boolean($v.value[prop] && !$v.value[prop][key].required.$invalid) || 'This field is required'
24
+ },
25
+ minLengthRule: (prop: string, key: string): boolean | string => {
26
+ const min = validations.addressLocal[key].max
27
+ const msg = min ? `Minimum length is ${min}` : 'Text is below minimum length'
28
+ return Boolean($v.value[prop] && !$v.value[prop][key].minLength.$invalid) || msg
29
+ },
30
+ maxLengthRule: (prop: string, key: string): boolean | string => {
31
+ const max = validations.addressLocal[key].max
32
+ const msg = max ? `Maximum length is ${max}` : 'Text is over maximum length'
33
+ return Boolean($v.value[prop] && !$v.value[prop][key].maxLength.$invalid) || msg
34
+ },
35
+ // FUTURE: generalize this rule to take a validation parameter (ie, 'CA')
36
+ isCanadaRule: (prop: string, key: string): boolean | string => {
37
+ return Boolean($v.value[prop] && !$v.value[prop][key].isCanada.$invalid) || 'Address must be in Canada'
38
+ },
39
+ // FUTURE: generalize this rule to take a validation parameter (ie, 'BC')
40
+ isBCRule: (prop: string, key: string): boolean | string => {
41
+ return Boolean($v.value[prop] && !$v.value[prop][key].isBC.$invalid) || 'Address must be in BC'
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Creates a Vuetify rules object from the Vuelidate state.
47
+ * @param model The name of the model we are validating.
48
+ * @returns A Vuetify rules object.
49
+ */
50
+ const createVuetifyRulesObject = (model: string): { [attr: string]: Array<Function> } => {
51
+ const obj = {
52
+ street: [],
53
+ streetAdditional: [],
54
+ city: [],
55
+ region: [],
56
+ postalCode: [],
57
+ country: [],
58
+ deliveryInstructions: []
59
+ }
60
+
61
+ // ensure Vuelidate state object is initialized
62
+ if ($v && $v.value[model]) {
63
+ // iterate over Vuelidate object properties
64
+ Object.keys($v.value[model])
65
+ // only look at validation properties
66
+ .filter(prop => prop.charAt(0) !== '$')
67
+ .forEach(prop => {
68
+ // create array for each validation property
69
+ obj[prop] = []
70
+ // iterate over validation property params
71
+ Object.keys($v.value[model][prop])
72
+ .forEach(param => {
73
+ // add specified validation functions to array
74
+ switch (param) {
75
+ case 'required': obj[prop].push(() => vuetifyRules.requiredRule(model, prop)); break
76
+ case 'minLength': obj[prop].push(() => vuetifyRules.minLengthRule(model, prop)); break
77
+ case 'maxLength': obj[prop].push(() => vuetifyRules.maxLengthRule(model, prop)); break
78
+ case 'isCanada': obj[prop].push(() => vuetifyRules.isCanadaRule(model, prop)); break
79
+ case 'isBC': obj[prop].push(() => vuetifyRules.isBCRule(model, prop)); break
80
+ // FUTURE: add extra validation functions here
81
+ default: break
82
+ }
83
+ })
84
+ })
85
+ }
86
+
87
+ // sample return object
88
+ // street: [
89
+ // () => this.requiredRule('addressLocal', 'street'),
90
+ // () => this.minLengthRule('addressLocal', 'street'),
91
+ // () => this.maxLengthRule('addressLocal', 'street')
92
+ // ],
93
+ // ...
94
+
95
+ return obj
96
+ }
97
+
98
+ /**
99
+ * The Vuetify rules object. Used to display any validation errors/styling.
100
+ * NB: As a getter, this is initialized between created() and mounted().
101
+ * @returns the Vuetify validation rules object
102
+ */
103
+ const rules = computed((): { [attr: string]: Array<Function> } => {
104
+ return createVuetifyRulesObject('addressLocal')
105
+ })
106
+
107
+ /** Array of validation rules used by input elements to prevent extra whitespace. */
108
+ const spaceRules = [
109
+ (v: string) => !/^\s/g.test(v) || 'Invalid spaces', // leading spaces
110
+ (v: string) => !/\s$/g.test(v) || 'Invalid spaces', // trailing spaces
111
+ (v: string) => !/\s\s/g.test(v) || 'Invalid word spacing' // multiple inline spaces
112
+ ]
113
+ return {
114
+ $v,
115
+ rules,
116
+ spaceRules
117
+ }
118
+ }
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@bcrs-shared-components/base-address",
3
- "version": "2.0.11",
3
+ "version": "3.0.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
7
  "dependencies": {
8
- "@bcrs-shared-components/mixins": "^1.1.29",
8
+ "@bcrs-shared-components/enums": "^2.0.0",
9
+ "@bcrs-shared-components/interfaces": "^2.0.0",
10
+ "@bcrs-shared-components/mixins": "^2.0.0",
11
+ "@vuelidate/core": "^2.0.3",
9
12
  "lodash.uniqueid": "^4.0.1",
10
13
  "vue": "^2.7.14",
11
14
  "vuelidate": "0.6.2"
@@ -14,5 +17,5 @@
14
17
  "vue-property-decorator": "^9.1.2",
15
18
  "vuelidate-property-decorators": "1.0.28"
16
19
  },
17
- "gitHead": "8dc1f0a920dcccabd46b1cfe57eb733131a4f6b7"
20
+ "gitHead": "79d7f9c9872f9eb873383c49c9d52ea94f731f90"
18
21
  }
@@ -0,0 +1,42 @@
1
+ import { AddressValidationRules } from '@bcrs-shared-components/enums'
2
+ import { SchemaIF } from '@bcrs-shared-components/interfaces'
3
+ import { baseRules, spaceRules } from '../factories/validation-factory'
4
+
5
+ /* example of what to pass in for the schema */
6
+ export const DefaultSchema: SchemaIF = {
7
+ street: [
8
+ baseRules[AddressValidationRules.REQUIRED],
9
+ baseRules[AddressValidationRules.MAX_LENGTH](50),
10
+ ...spaceRules
11
+ ],
12
+ streetAdditional: [
13
+ baseRules[AddressValidationRules.MAX_LENGTH](50),
14
+ ...spaceRules
15
+ ],
16
+ city: [
17
+ baseRules[AddressValidationRules.REQUIRED],
18
+ baseRules[AddressValidationRules.MAX_LENGTH](40),
19
+ ...spaceRules
20
+ ],
21
+ country: [
22
+ baseRules[AddressValidationRules.REQUIRED],
23
+ ...spaceRules
24
+ ],
25
+ region: [
26
+ baseRules[AddressValidationRules.REQUIRED],
27
+ ...spaceRules
28
+ ],
29
+ /* NOTE: Canada/US postal code and zip code regex rules
30
+ * are added automatically as extra rules based on country
31
+ * inside the address components
32
+ */
33
+ postalCode: [
34
+ baseRules[AddressValidationRules.REQUIRED],
35
+ baseRules[AddressValidationRules.MAX_LENGTH](15),
36
+ ...spaceRules
37
+ ],
38
+ deliveryInstructions: [
39
+ baseRules[AddressValidationRules.MAX_LENGTH](80),
40
+ ...spaceRules
41
+ ]
42
+ }
@@ -0,0 +1 @@
1
+ export { DefaultSchema } from './default-schema'