Skip to content

Commit 8afbdb1

Browse files
committed
Polished the matrix input
1 parent 88ec9a3 commit 8afbdb1

File tree

13 files changed

+487
-236
lines changed

13 files changed

+487
-236
lines changed

app/Http/Requests/AnswerFormRequest.php

+4-1
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,13 @@ public function rules()
8484
// For star rating, needs a minimum of 1 star
8585
$rules[] = 'min:1';
8686
} elseif ($property['type'] == 'matrix') {
87-
$rules[] = new MatrixValidationRule();
87+
$rules[] = new MatrixValidationRule($property, true);
8888
}
8989
} else {
9090
$rules[] = 'nullable';
91+
if ($property['type'] == 'matrix') {
92+
$rules[] = new MatrixValidationRule($property, false);
93+
}
9194
}
9295

9396
// Clean id to escape "."

app/Rules/MatrixValidationRule.php

+43-10
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,55 @@
77

88
class MatrixValidationRule implements ValidationRule
99
{
10-
/**
11-
* Run the validation rule.
12-
*
13-
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
14-
*/
10+
protected $field;
11+
protected $isRequired;
12+
13+
public function __construct(array $field, bool $isRequired)
14+
{
15+
$this->field = $field;
16+
$this->isRequired = $isRequired;
17+
}
18+
1519
public function validate(string $attribute, mixed $value, Closure $fail): void
1620
{
21+
if (!$this->isRequired && empty($value)) {
22+
return; // If not required and empty, validation passes
23+
}
24+
1725
if (!is_array($value)) {
1826
$fail('The Matrix field must be an array.');
1927
return;
2028
}
21-
$nullValues = array_filter($value, function ($val) {
22-
return $val === null;
23-
});
24-
if (sizeof($nullValues)) {
25-
$fail('The Matrix field is required.');
29+
30+
$rows = $this->field['rows'];
31+
$columns = $this->field['columns'];
32+
33+
foreach ($rows as $row) {
34+
if (!array_key_exists($row, $value)) {
35+
if ($this->isRequired) {
36+
$fail("Missing value for row '{$row}'.");
37+
}
38+
continue;
39+
}
40+
41+
$cellValue = $value[$row];
42+
43+
if ($cellValue === null) {
44+
if ($this->isRequired) {
45+
$fail("Value for row '{$row}' is required.");
46+
}
47+
continue;
48+
}
49+
50+
if (!in_array($cellValue, $columns)) {
51+
$fail("Invalid value '{$cellValue}' for row '{$row}'.");
52+
}
53+
}
54+
55+
// Check for extra rows that shouldn't be there
56+
$extraRows = array_diff(array_keys($value), $rows);
57+
foreach ($extraRows as $extraRow) {
58+
$fail("Unexpected row '{$extraRow}' in the matrix.");
2659
}
2760
}
2861
}

client/components/forms/FlatSelectInput.vue

+37-49
Original file line numberDiff line numberDiff line change
@@ -21,54 +21,40 @@
2121
},
2222
]"
2323
>
24-
<template
25-
v-if="options && options.length"
26-
>
27-
<div
28-
v-for="(option) in options"
29-
:key="option[optionKey]"
30-
:role="multiple?'checkbox':'radio'"
31-
:aria-checked="isSelected(option[optionKey])"
32-
:class="[
33-
theme.FlatSelectInput.spacing.vertical,
34-
theme.FlatSelectInput.fontSize,
35-
theme.FlatSelectInput.option,
36-
]"
37-
@click="onSelect(option[optionKey])"
24+
<template
25+
v-if="options && options.length"
3826
>
39-
<template v-if="multiple">
40-
<Icon
41-
v-if="isSelected(option[optionKey])"
42-
name="material-symbols:check-box"
43-
class="text-inherit"
44-
:color="color"
45-
:class="[theme.FlatSelectInput.icon]"
46-
/>
47-
<Icon
48-
v-else
49-
name="material-symbols:check-box-outline-blank"
50-
:class="[theme.FlatSelectInput.icon,theme.FlatSelectInput.unselectedIcon]"
51-
/>
52-
</template>
53-
<template v-else>
54-
<Icon
55-
v-if="isSelected(option[optionKey])"
56-
name="material-symbols:radio-button-checked-outline"
57-
class="text-inherit"
58-
:color="color"
59-
:class="[theme.FlatSelectInput.icon]"
60-
/>
61-
<Icon
62-
v-else
63-
name="material-symbols:radio-button-unchecked"
64-
:class="[theme.FlatSelectInput.icon,theme.FlatSelectInput.unselectedIcon]"
65-
/>
66-
</template>
67-
<p class="flex-grow">
68-
{{ option[displayKey] }}
69-
</p>
70-
</div>
71-
</template>
27+
<div
28+
v-for="(option) in options"
29+
:key="option[optionKey]"
30+
:role="multiple?'checkbox':'radio'"
31+
:aria-checked="isSelected(option[optionKey])"
32+
:class="[
33+
theme.FlatSelectInput.spacing.vertical,
34+
theme.FlatSelectInput.fontSize,
35+
theme.FlatSelectInput.option,
36+
]"
37+
@click="onSelect(option[optionKey])"
38+
>
39+
<template v-if="multiple">
40+
<CheckboxIcon
41+
:is-checked="isSelected(option[optionKey])"
42+
:color="color"
43+
:theme="theme"
44+
/>
45+
</template>
46+
<template v-else>
47+
<RadioButtonIcon
48+
:is-checked="isSelected(option[optionKey])"
49+
:color="color"
50+
:theme="theme"
51+
/>
52+
</template>
53+
<p class="flex-grow">
54+
{{ option[displayKey] }}
55+
</p>
56+
</div>
57+
</template>
7258
<div
7359
v-else
7460
:class="[
@@ -95,13 +81,15 @@
9581
<script>
9682
import {inputProps, useFormInput} from "./useFormInput.js"
9783
import InputWrapper from "./components/InputWrapper.vue"
84+
import RadioButtonIcon from "./components/RadioButtonIcon.vue"
85+
import CheckboxIcon from "./components/CheckboxIcon.vue"
9886
9987
/**
10088
* Options: {name,value} objects
10189
*/
10290
export default {
10391
name: "FlatSelectInput",
104-
components: {InputWrapper},
92+
components: {InputWrapper, RadioButtonIcon, CheckboxIcon},
10593
10694
props: {
10795
...inputProps,
@@ -155,4 +143,4 @@ export default {
155143
},
156144
},
157145
}
158-
</script>
146+
</script>
+75-71
Original file line numberDiff line numberDiff line change
@@ -1,117 +1,121 @@
11
<template>
22
<input-wrapper v-bind="inputWrapperProps">
33
<template #label>
4-
<slot name="label"/>
4+
<slot name="label" />
55
</template>
6-
<div class='border' :class="[
6+
<div
7+
class="border border-gray-300"
8+
:class="[
79
theme.default.borderRadius,
810
{
911
'!ring-red-500 !ring-2 !border-transparent': hasError,
10-
'!cursor-not-allowed !bg-gray-200': disabled,
12+
'!cursor-not-allowed !bg-gray-300': disabled,
1113
},
12-
]">
13-
<table
14-
class="w-full table-fixed overflow-hidden border border-collapse border-transparent"
15-
:class="[
16-
theme.default.borderRadius]"
14+
]"
1715
>
18-
<thead class="">
19-
<tr class="bg-gray-200/60">
20-
<th>
21-
22-
</th>
23-
<td v-for="column in columns" :key="column" class="border border-gray-300">
24-
<div class="p-2 w-full flex items-center justify-center capitalize text-sm truncate">
25-
{{ column }}
26-
</div>
27-
</td>
28-
</tr>
29-
</thead>
16+
<table
17+
class="w-full table-fixed overflow-hidden"
18+
>
19+
<thead class="">
20+
<tr>
21+
<th />
22+
<td
23+
v-for="column in columns"
24+
:key="column"
25+
class="border-l border-gray-300"
26+
>
27+
<div class="p-2 w-full flex items-center justify-center capitalize text-sm truncate">
28+
{{ column }}
29+
</div>
30+
</td>
31+
</tr>
32+
</thead>
3033

31-
<tbody>
32-
<tr v-for="row, rowIndex in rows" :key="rowIndex" class="">
33-
<td class="border border-gray-300">
34-
<div class="w-full flex-grow p-2 text-sm truncate">
35-
{{ row }}
36-
</div>
37-
</td>
38-
<td v-for="column in columns" :key="row + column" class="border border-gray-300">
39-
<div
40-
class="w-full flex items-center justify-center hover:bg-gray-200/40"
41-
v-if="compVal"
42-
role="radio"
43-
:aria-checked="compVal[row] === column"
44-
:class="[
45-
theme.FlatSelectInput.spacing.vertical,
46-
theme.FlatSelectInput.fontSize,
47-
theme.FlatSelectInput.option,
48-
]"
49-
@click="onSelect(row, column)"
34+
<tbody>
35+
<tr
36+
v-for="row, rowIndex in rows"
37+
:key="rowIndex"
38+
class="border-t border-gray-300"
39+
>
40+
<td>
41+
<div class="w-full flex-grow p-2 text-sm truncate">
42+
{{ row }}
43+
</div>
44+
</td>
45+
<td
46+
v-for="column in columns"
47+
:key="row + column"
48+
class="border-l border-gray-300 hover:!bg-gray-100 dark:hover:!bg-gray-800"
5049
>
51-
<Icon
52-
v-if="compVal[row] === column"
53-
:key="row+column"
54-
name="material-symbols:radio-button-checked-outline"
55-
class="text-inherit"
56-
:color="color"
57-
:class="[theme.FlatSelectInput.icon]"
58-
/>
59-
<Icon
60-
v-else
61-
:key="row+column"
62-
name="material-symbols:radio-button-unchecked"
63-
:class="[theme.FlatSelectInput.icon,theme.FlatSelectInput.unselectedIcon]"
64-
/>
65-
</div>
66-
</td>
67-
</tr>
68-
</tbody>
69-
</table>
50+
<div
51+
v-if="compVal"
52+
class="w-full flex items-center justify-center"
53+
role="radio"
54+
:aria-checked="compVal[row] === column"
55+
:class="[
56+
theme.FlatSelectInput.spacing.vertical,
57+
theme.FlatSelectInput.fontSize,
58+
theme.FlatSelectInput.option,
59+
]"
60+
@click="onSelect(row, column)"
61+
>
62+
<RadioButtonIcon
63+
:key="row+column"
64+
:is-checked="compVal[row] === column"
65+
:color="color"
66+
:theme="theme"
67+
/>
68+
</div>
69+
</td>
70+
</tr>
71+
</tbody>
72+
</table>
7073
</div>
7174
<template #help>
72-
<slot name="help"/>
75+
<slot name="help" />
7376
</template>
7477
<template #error>
75-
<slot name="error"/>
78+
<slot name="error" />
7679
</template>
7780
</input-wrapper>
7881
</template>
7982
<script>
8083
import {inputProps, useFormInput} from "./useFormInput.js"
8184
import InputWrapper from "./components/InputWrapper.vue"
85+
import RadioButtonIcon from "./components/RadioButtonIcon.vue"
8286
8387
export default {
8488
name: "MatrixInput",
85-
components: {InputWrapper},
89+
components: {InputWrapper, RadioButtonIcon},
8690
8791
props: {
8892
...inputProps,
8993
rows: {type: Array, required: true},
9094
columns: {type: Array, required: true},
9195
},
92-
data() {
96+
setup(props, context) {
9397
return {
98+
...useFormInput(props, context),
9499
}
95100
},
96-
setup(props, context) {
101+
data() {
97102
return {
98-
...useFormInput(props, context),
99103
}
100104
},
101105
computed: {},
106+
beforeMount() {
107+
if (!this.compVal || typeof this.compVal !== 'object') {
108+
this.compVal = {}
109+
}
110+
},
102111
methods: {
103112
onSelect(row, column) {
104-
if (this.compVal[row] === column) {
113+
if (this.compVal[row] === column && !this.required) {
105114
this.compVal[row] = null
106115
} else {
107116
this.compVal[row] = column
108117
}
109118
},
110119
},
111-
beforeMount() {
112-
if (!this.compVal || typeof this.compVal !== 'object') {
113-
this.compVal = {}
114-
}
115-
},
116120
}
117-
</script>
121+
</script>

0 commit comments

Comments
 (0)