@westartcom/bread-quasar-fields 0.0.2

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,158 @@
1
+ <template>
2
+ <model-form
3
+ :key="field.name + '-form'"
4
+ :definition="nestedDefinition"
5
+ :model="internalValue"
6
+ :errors="errors"
7
+ v-model:invalid="invalid">
8
+ <template v-for="(_, name) in $slots" #[name]="slotData"><slot :name="name" v-bind="slotData" /></template>
9
+ </model-form>
10
+ </template>
11
+
12
+ <script lang="ts">
13
+ import { defineAsyncComponent, defineComponent, PropType } from 'vue';
14
+ import forEach from 'lodash-es/forEach';
15
+ import flatMap from 'lodash-es/flatMap';
16
+ import find from 'lodash-es/find';
17
+ import { ModelDefinition, ModelFieldDefinition, ModelFieldGroupDefinition } from '@westartcom/bread-quasar-fields/interfaces/ModelDefinition';
18
+ import { FieldType } from '@westartcom/bread-quasar-fields/enums/FieldType';
19
+ import { InputType } from '@westartcom/bread-quasar-fields/enums/InputType';
20
+
21
+ interface NestedFieldsFieldDefinition extends ModelFieldDefinition<FieldType.JSON | FieldType.META, InputType.NESTED_FIELDS> {
22
+ extra_data: {
23
+ fields: ModelFieldDefinition[] | {
24
+ [key: string]: ModelFieldGroupDefinition;
25
+ }
26
+ }
27
+ }
28
+
29
+ export default defineComponent({
30
+ components: {
31
+ ModelForm: defineAsyncComponent(() => import('@/Components/ModelForm.vue')),
32
+ },
33
+
34
+ props: {
35
+ field: {
36
+ type: Object as PropType<NestedFieldsFieldDefinition>,
37
+ required: true
38
+ },
39
+ modelValue: [String, Object, Array],
40
+ errors: Object as PropType<{[key: string]: string[]}[]>
41
+ },
42
+
43
+ emits: [
44
+ 'update:modelValue',
45
+ 'update:invalid'
46
+ ],
47
+
48
+ data() {
49
+ return {
50
+ invalid: null as boolean | null,
51
+ internalValue: {},
52
+ internalErrors: [] as {[key: string]: string[]}[],
53
+ nestedDefinition: null as Pick<ModelDefinition, 'field_groups'> | null,
54
+ originalStringifiedValue: ''
55
+ };
56
+ },
57
+
58
+ mounted() {
59
+ this.internalValue = this.formatData(this.modelValue);
60
+ this.nestedDefinition = {
61
+ field_groups: (Array.isArray(this.field.extra_data.fields)) ? {
62
+ 'general': {
63
+ key: 'general',
64
+ order: 0,
65
+ fields: this.field.extra_data.fields
66
+ }
67
+ } : this.field.extra_data.fields
68
+ };
69
+ },
70
+
71
+ watch: {
72
+ modelValue: {
73
+ handler: function(val) {
74
+ const stringifiedVal = JSON.stringify(val);
75
+ if (stringifiedVal !== this.originalStringifiedValue) {
76
+ if (val && Array.isArray(val)) {
77
+ this.internalValue = this.formatData(val);
78
+ } else {
79
+ if (typeof val === 'object') {
80
+ this.internalValue = val;
81
+ }
82
+ }
83
+ }
84
+ },
85
+ deep: true
86
+ },
87
+
88
+ internalValue: {
89
+ handler: function(val) {
90
+ this.$emit('update:modelValue', val);
91
+ },
92
+ deep: true
93
+ },
94
+
95
+ errors: {
96
+ handler: function(val) {
97
+ this.internalErrors = val;
98
+ },
99
+ deep: true
100
+ },
101
+
102
+ internalErrors: {
103
+ handler: function(val) {
104
+ this.invalid = !!(val && val.length);
105
+ },
106
+ deep: true
107
+ },
108
+
109
+ invalid: function(val) {
110
+ this.$emit('update:invalid', val);
111
+ }
112
+ },
113
+
114
+ methods: {
115
+ findDefinitionForField(fieldName): ModelFieldDefinition | undefined {
116
+ const fields = (Array.isArray(this.field.extra_data.fields)) ? this.field.extra_data.fields : flatMap(this.field.extra_data.fields, (group) => group.fields);
117
+ return find(fields, ['name', fieldName]);
118
+ },
119
+
120
+ formatData(nestedData) {
121
+ let formattedValue = {};
122
+
123
+ if (nestedData && Array.isArray(nestedData)) {
124
+ forEach(nestedData, (data) => {
125
+ if (!data?.meta_key) {
126
+ return;
127
+ }
128
+
129
+ const definition = this.findDefinitionForField(data.meta_key);
130
+ if (!definition) {
131
+ return;
132
+ }
133
+
134
+ if (definition.type !== 'MEDIA') {
135
+ formattedValue[data.meta_key] = data.meta_value;
136
+ return;
137
+ }
138
+
139
+ if (data.hasOwnProperty(definition.extra_data?.collection)) {
140
+ formattedValue[data.meta_key] = data[definition.extra_data?.collection];
141
+ } else {
142
+ formattedValue[data.meta_key] = data.meta_value;
143
+ }
144
+ });
145
+ } else {
146
+ formattedValue = (typeof nestedData !== 'undefined') ? nestedData : {};
147
+ }
148
+
149
+ this.originalStringifiedValue = JSON.stringify(formattedValue);
150
+
151
+ return formattedValue;
152
+ },
153
+ }
154
+ });
155
+ </script>
156
+
157
+ <style scoped lang="scss">
158
+ </style>
@@ -0,0 +1,116 @@
1
+ <template>
2
+ <div class="field-wrap" :id="id">
3
+ <div class="field-wrap-inner">
4
+ <div class="field-label-wrap">
5
+ <label v-if="field.label" :for="id + '-input'">{{ field.label }}<span class="required-asterix" v-if="field.required"> *</span></label>
6
+ </div>
7
+
8
+ <input
9
+ :name="field.name"
10
+ :id="id + '-input'"
11
+ :placeholder="field.placeholder"
12
+ :max="(field.extra_data && field.extra_data.max) ? field.extra_data.max : null"
13
+ :min="(field.extra_data && field.extra_data.min) ? field.extra_data.min : null"
14
+ type="number"
15
+ class="field"
16
+ :class="{'invalid': (invalid && internalErrors && internalErrors.length)}"
17
+ v-model="internalValue">
18
+ </div>
19
+
20
+ <div class="field-description" v-if="field.description">{{ field.description }}</div>
21
+
22
+ <div class="validation-errors" v-if="invalid && internalErrors && internalErrors.length">
23
+ <span class="error danger-txt-color" v-for="(error, index) in internalErrors" :key="index + 'error'">{{ error }}</span>
24
+ </div>
25
+ </div>
26
+ </template>
27
+
28
+ <script lang="ts">
29
+ import { defineComponent, PropType } from 'vue';
30
+ import { ModelFieldDefinition } from '@westartcom/bread-quasar-fields/interfaces/ModelDefinition';
31
+ import { FieldType } from '@westartcom/bread-quasar-fields/enums/FieldType';
32
+ import { InputType } from '@westartcom/bread-quasar-fields/enums/InputType';
33
+
34
+ interface NumberFieldDefinition extends ModelFieldDefinition<FieldType.FLOAT | FieldType.INT, InputType.NUMBER> {
35
+ default: number | null;
36
+ }
37
+
38
+ export default defineComponent({
39
+ props: {
40
+ id: {
41
+ type: String,
42
+ required: true
43
+ },
44
+ field: {
45
+ type: Object as PropType<NumberFieldDefinition>,
46
+ required: true
47
+ },
48
+ modelValue: [String, Number],
49
+ errors: Array as PropType<string[]>
50
+ },
51
+
52
+ emits: [
53
+ 'update:modelValue',
54
+ 'update:invalid'
55
+ ],
56
+
57
+ data() {
58
+ return {
59
+ invalid: null as boolean | null,
60
+ internalValue: null as number | null,
61
+ internalErrors: [] as string[]
62
+ };
63
+ },
64
+
65
+ mounted() {
66
+ if (this.modelValue) {
67
+ this.internalValue = Number(this.modelValue);
68
+ } else if (this.field.default) {
69
+ this.internalValue = Number(this.field.default);
70
+ }
71
+
72
+ this.validate(this.internalValue);
73
+ },
74
+
75
+ watch: {
76
+ modelValue: function(val) {
77
+ if (val !== this.internalValue) {
78
+ this.internalValue = Number(val);
79
+ }
80
+ },
81
+
82
+ internalValue: function(val) {
83
+ this.validate(val);
84
+ this.$emit('update:modelValue', Number(val));
85
+ },
86
+
87
+ errors: {
88
+ handler: function(val) {
89
+ this.internalErrors = val;
90
+ },
91
+ deep: true
92
+ },
93
+
94
+ internalErrors: {
95
+ handler: function(val) {
96
+ this.invalid = !!(val && val.length);
97
+ },
98
+ deep: true
99
+ },
100
+
101
+ invalid: function(val) {
102
+ this.$emit('update:invalid', val);
103
+ }
104
+ },
105
+
106
+ methods: {
107
+ validate: function(val) {
108
+ this.invalid = ((this.field.required && !val) || (val && Number.isNaN(val)));
109
+ }
110
+ }
111
+ });
112
+ </script>
113
+
114
+ <style scoped lang="scss">
115
+
116
+ </style>
@@ -0,0 +1,201 @@
1
+ <template>
2
+ <div class="field-wrap" :id="id">
3
+ <div class="field-wrap-inner">
4
+ <div class="field-label-wrap">
5
+ <label v-if="field.label">
6
+ {{ field.label }}<span class="required-asterix" v-if="field.required"> *</span>
7
+ </label>
8
+ </div>
9
+
10
+ <template v-if="stylingFormat === 'buttons' || stylingFormat === 'toggle'">
11
+ <div :class="stylingFormat">
12
+ <button
13
+ v-for="(label, optionValue) in allOptions"
14
+ :key="id + '-' + optionValue + '-radio-input'"
15
+ type="button"
16
+ class="btn expand"
17
+ @click="internalValue = optionValue"
18
+ :class="{
19
+ 'normal-filled': (internalValue === optionValue && !invalid),
20
+ 'hollow-danger': (internalValue === optionValue && invalid),
21
+ 'hollow': (internalValue !== optionValue)
22
+ }">
23
+ {{ label }}
24
+ </button>
25
+ </div>
26
+ </template>
27
+
28
+ <template v-else>
29
+ <div class="field">
30
+ <label
31
+ v-for="(label, optionValue) in allOptions"
32
+ :key="id + '-' + optionValue + '-radio-input'"
33
+ :for="id + '-' + optionValue + '-radio-input'">
34
+ <input
35
+ :id="id + '-' + optionValue + '-radio-input'"
36
+ type="radio"
37
+ :value="optionValue"
38
+ v-model="internalValue">
39
+
40
+ {{ label }}
41
+ </label>
42
+ </div>
43
+ </template>
44
+ </div>
45
+
46
+ <div class="field-description" v-if="field.description">{{ field.description }}</div>
47
+
48
+ <div class="validation-errors" v-if="invalid && internalErrors && internalErrors.length">
49
+ <span class="error danger-txt-color" v-for="(error, index) in internalErrors" :key="index + 'error'">{{ error }}</span>
50
+ </div>
51
+ </div>
52
+ </template>
53
+
54
+ <script lang="ts">
55
+ import { defineComponent, PropType } from 'vue';
56
+ import { ModelFieldDefinition } from '@westartcom/bread-quasar-fields/interfaces/ModelDefinition';
57
+ import { FieldType } from '@westartcom/bread-quasar-fields/enums/FieldType';
58
+ import { InputType } from '@westartcom/bread-quasar-fields/enums/InputType';
59
+
60
+ interface RadioFieldDefinition extends ModelFieldDefinition<FieldType.ENUM, InputType.RADIO> {
61
+ default: string | number;
62
+ extra_data: {
63
+ styling_format: 'radio' | 'buttons' | 'toggle';
64
+ options: {
65
+ [key: string]: string | number
66
+ }
67
+ }
68
+ }
69
+
70
+ export default defineComponent({
71
+ props: {
72
+ id: {
73
+ type: String,
74
+ required: true
75
+ },
76
+ field: {
77
+ type: Object as PropType<RadioFieldDefinition>,
78
+ required: true
79
+ },
80
+ modelValue: [String, Number],
81
+ errors: Array as PropType<string[]>
82
+ },
83
+
84
+ emits: [
85
+ 'update:modelValue',
86
+ 'update:invalid'
87
+ ],
88
+
89
+ data() {
90
+ return {
91
+ invalid: null as boolean | null,
92
+ internalValue: null as string | null,
93
+ internalErrors: [] as string[]
94
+ };
95
+ },
96
+
97
+ mounted() {
98
+ if (this.modelValue) {
99
+ this.internalValue = this.modelValue.toString();
100
+ } else if (this.field.default) {
101
+ this.internalValue = this.field.default.toString();
102
+ }
103
+
104
+ this.validate(this.internalValue);
105
+ },
106
+
107
+ computed: {
108
+ stylingFormat: function() {
109
+ return (this.field.extra_data?.styling_format) ? this.field.extra_data.styling_format : 'radio';
110
+ },
111
+
112
+ allOptions: function() {
113
+ return (this.field.extra_data?.options) ? this.field.extra_data.options : {};
114
+ }
115
+ },
116
+
117
+ watch: {
118
+ modelValue: function(val) {
119
+ if (val !== this.internalValue) {
120
+ this.internalValue = val;
121
+ }
122
+ },
123
+
124
+ internalValue: function(val) {
125
+ this.validate(val);
126
+ this.$emit('update:modelValue', val);
127
+ },
128
+
129
+ errors: {
130
+ handler: function(val) {
131
+ this.internalErrors = val;
132
+ },
133
+ deep: true
134
+ },
135
+
136
+ internalErrors: {
137
+ handler: function(val) {
138
+ this.invalid = !!(val && val.length);
139
+ },
140
+ deep: true
141
+ },
142
+
143
+ invalid: function(val) {
144
+ this.$emit('update:invalid', val);
145
+ }
146
+ },
147
+
148
+ methods: {
149
+ validate: function(val) {
150
+ this.invalid = this.field.required && !val;
151
+ }
152
+ }
153
+ });
154
+ </script>
155
+
156
+ <style scoped lang="scss">
157
+ .toggle {
158
+ display: flex;
159
+ max-width: var(--max-input-width);
160
+ width: 100%;
161
+
162
+
163
+ .btn {
164
+ border-radius: 0;
165
+ padding-right: 0;
166
+ padding-left: 0;
167
+
168
+ &:not(:first-of-type) {
169
+ border-left: 0;
170
+ }
171
+
172
+ &:first-of-type {
173
+ border-bottom-left-radius: var(--global-radius);
174
+ border-top-left-radius: var(--global-radius);
175
+ }
176
+
177
+ &:last-of-type {
178
+ border-bottom-right-radius: var(--global-radius);
179
+ border-top-right-radius: var(--global-radius);
180
+ }
181
+ }
182
+ }
183
+
184
+ .buttons {
185
+ display: flex;
186
+ max-width: var(--max-input-width);
187
+ width: 100%;
188
+ justify-content: space-between;
189
+ flex-wrap: wrap;
190
+
191
+ .btn {
192
+ box-sizing: border-box;
193
+ width: calc(50% - 8px);
194
+ margin-bottom: var(--global-spacing);
195
+ }
196
+ }
197
+
198
+ .field > label {
199
+ margin-right: var(--global-spacing);
200
+ }
201
+ </style>
@@ -0,0 +1,185 @@
1
+ <template>
2
+ <div class="repeatable-nested-fields">
3
+ <div class="field-label-wrap">
4
+ <label v-if="field.label">{{ field.label }}<span class="required-asterix" v-if="field.required"> *</span></label>
5
+ </div>
6
+
7
+ <div class="field-description" v-if="field.description">{{ field.description }}</div>
8
+
9
+ <button class="add-row btn primary" type="button" @click="addRow">
10
+ <template v-if="field.extra_data && field.extra_data.add_label">
11
+ {{ field.extra_data.add_label }}
12
+ </template>
13
+ <template v-else>
14
+ {{ $i18n.get('actions.add') }}
15
+ </template>
16
+ </button>
17
+
18
+ <div class="repeatable-row" v-for="(row, index) in internalValue" :key="field.name + '-row-' + index">
19
+ <div class="actions">
20
+ <button type="button" class="btn remove-row" :title="$i18n.get('actions.remove')" @click="removeRow(row, index)">
21
+ <delete-icon class="icon-1-5x" :title="$i18n.get('actions.remove')"></delete-icon>
22
+ </button>
23
+ </div>
24
+
25
+ <model-form
26
+ :key="field.name + '-form-' + index"
27
+ :definition="nestedDefinition"
28
+ :model="row"
29
+ :errors="internalErrors[index]"
30
+ v-model:invalid="invalid">
31
+ </model-form>
32
+ </div>
33
+ </div>
34
+ </template>
35
+
36
+ <script lang="ts">
37
+ import { defineAsyncComponent, defineComponent, PropType } from 'vue';
38
+ import { ModelDefinition, ModelFieldDefinition } from '@westartcom/bread-quasar-fields/interfaces/ModelDefinition';
39
+ import { FieldType } from '@westartcom/bread-quasar-fields/enums/FieldType';
40
+ import { InputType } from '@westartcom/bread-quasar-fields/enums/InputType';
41
+
42
+ interface NestedFieldsFieldDefinition extends ModelFieldDefinition<FieldType.JSON, InputType.NESTED_FIELDS> {
43
+ extra_data: {
44
+ fields: ModelFieldDefinition[];
45
+ add_label?: string;
46
+ }
47
+ }
48
+
49
+ export default defineComponent({
50
+ components: {
51
+ DeleteIcon: defineAsyncComponent(() => import('@westartcom/bread-quasar-fields/icons/DeleteIcon.vue')),
52
+ ModelForm: defineAsyncComponent(() => import('@/Components/ModelForm.vue')),
53
+ },
54
+
55
+ props: {
56
+ field: {
57
+ type: Object as PropType<NestedFieldsFieldDefinition>,
58
+ required: true
59
+ },
60
+ modelValue: Array,
61
+ errors: Object as PropType<{[key: string]: string[]}[]>
62
+ },
63
+
64
+ emits: [
65
+ 'update:modelValue',
66
+ 'update:invalid'
67
+ ],
68
+
69
+ data() {
70
+ return {
71
+ invalid: null as boolean | null,
72
+ internalValue: {} as {[key: string]: any}[],
73
+ internalErrors: [] as {[key: string]: string[]}[],
74
+ nestedDefinition: null as Pick<ModelDefinition, 'field_groups'> | null,
75
+ };
76
+ },
77
+
78
+ mounted() {
79
+ this.internalValue = (this.modelValue && Array.isArray(this.modelValue)) ? this.modelValue : [];
80
+ this.nestedDefinition = {
81
+ field_groups: {
82
+ general: {
83
+ key: 'general',
84
+ order: 0,
85
+ fields: this.field.extra_data.fields
86
+ }
87
+ }
88
+ };
89
+ },
90
+
91
+ watch: {
92
+ modelValue: {
93
+ handler: function(val) {
94
+ if (Array.isArray(val)) {
95
+ const stringifiedInternal = JSON.stringify(this.internalValue);
96
+ const stringifiedValue = JSON.stringify(val);
97
+
98
+ if (stringifiedInternal !== stringifiedValue) {
99
+ this.internalValue = val;
100
+ }
101
+ }
102
+ },
103
+ deep: true
104
+ },
105
+
106
+ internalValue: {
107
+ handler: function(val) {
108
+ this.$emit('update:modelValue', val);
109
+ },
110
+ deep: true
111
+ },
112
+
113
+ errors: {
114
+ handler: function(val) {
115
+ if (val && typeof val === 'object') {
116
+ const keys = Object.keys(val);
117
+ for (const index in keys) {
118
+ const key = keys[index];
119
+ if (key.includes('.')) {
120
+ const explodedKey = key.split('.');
121
+ const mainKey = explodedKey[0];
122
+
123
+ if (this.field.name === mainKey) {
124
+ if (!this.internalErrors[explodedKey[1]]) {
125
+ this.internalErrors[explodedKey[1]] = {};
126
+ }
127
+
128
+ this.internalErrors[explodedKey[1]][explodedKey[2]] = val[key];
129
+ }
130
+ }
131
+ }
132
+ } else {
133
+ this.internalErrors = [];
134
+ }
135
+ },
136
+ deep: true
137
+ },
138
+
139
+ internalErrors: {
140
+ handler: function(val) {
141
+ this.invalid = !!(val && val.length);
142
+ },
143
+ deep: true
144
+ },
145
+
146
+ invalid: function(val) {
147
+ this.$emit('update:invalid', val);
148
+ }
149
+ },
150
+
151
+ methods: {
152
+ addRow() {
153
+ this.internalValue.push({});
154
+ },
155
+
156
+ removeRow(row, index) {
157
+ this.internalValue.splice(index, 1);
158
+ }
159
+ }
160
+ });
161
+ </script>
162
+
163
+ <style scoped lang="scss">
164
+ .add-row {
165
+ display: block;
166
+ margin-bottom: var(--global-spacing);
167
+ }
168
+
169
+ .actions {
170
+ text-align: right;
171
+ }
172
+
173
+ .remove-row {
174
+ padding: var(--global-spacing-half);
175
+ margin: 0;
176
+ color: var(--color-danger);
177
+ }
178
+
179
+ .model-form {
180
+ padding-top: var(--global-spacing);
181
+ padding-bottom: var(--global-spacing);
182
+ margin-bottom: var(--global-spacing);
183
+ border-bottom: 1px solid var(--color-medium-tint);
184
+ }
185
+ </style>