Skip to content

Commit 731f17d

Browse files
committed
VInputFieldとInputNumberFieldをクライアント側から移植
1 parent f1e1d68 commit 731f17d

File tree

2 files changed

+314
-0
lines changed

2 files changed

+314
-0
lines changed

components/InputNumberField.vue

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<template>
2+
<validation-provider :name="name" :rules="rules" tag="div">
3+
<label :for="id" class="labelText">{{ label }}</label>
4+
<VInputField
5+
:id="id"
6+
type="number"
7+
:name="name"
8+
:placeholder="placeholder"
9+
:step="step"
10+
:value="value"
11+
:unit="unit"
12+
:required="required"
13+
:floating-point="floatingPoint"
14+
:pulse="pulse"
15+
:temperature="temperature"
16+
:spo2="spo2"
17+
@input="$emit('input', $event.target.value)"
18+
/>
19+
</validation-provider>
20+
</template>
21+
22+
<script lang="ts">
23+
import { Component, Prop, Vue } from 'vue-property-decorator'
24+
import VInputField from '@/components/VInputField.vue'
25+
@Component({
26+
components: {
27+
VInputField,
28+
},
29+
})
30+
export default class InputNumberField extends Vue {
31+
@Prop({ type: String, default: '' })
32+
id: string | undefined
33+
34+
@Prop({ type: String, default: '' })
35+
rules: string | undefined
36+
37+
@Prop({ type: String, default: 0 })
38+
value: string | undefined
39+
40+
@Prop({ type: String, default: '' })
41+
label: string | undefined
42+
43+
@Prop({ type: String, default: '' })
44+
name: string | undefined
45+
46+
@Prop({ type: String, default: '' })
47+
placeholder: string | undefined
48+
49+
@Prop({ type: String, default: '' })
50+
unit: string | undefined
51+
52+
@Prop({ type: Number, default: 0 })
53+
step: number | undefined
54+
55+
@Prop({ type: Boolean, default: false })
56+
required: boolean | undefined
57+
58+
@Prop({ type: Boolean, default: false })
59+
floatingPoint: boolean | undefined
60+
61+
@Prop({ type: Boolean, default: false })
62+
pulse: boolean | undefined
63+
64+
@Prop({ type: Boolean, default: false })
65+
temperature: boolean | undefined
66+
67+
@Prop({ type: Boolean, default: false })
68+
spo2: boolean | undefined
69+
}
70+
</script>
71+
72+
<style lang="scss" scoped>
73+
.labelText {
74+
display: block;
75+
margin: 0 0 8px;
76+
font-size: 16px;
77+
}
78+
</style>

components/VInputField.vue

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
<template>
2+
<div class="inputFieldOuter">
3+
<div class="inputFieldControl">
4+
<input
5+
:id="id"
6+
class="inputField"
7+
:class="{ 'inputField-error': showError }"
8+
:style="{ fontSize: fontSizeMap.get(fontSize) }"
9+
:type="type"
10+
:pattern="type === 'number' ? '\\d*' : null"
11+
:inputmode="type === 'number' ? 'decimal' : 'text'"
12+
:name="name"
13+
:placeholder="placeholder"
14+
:step="step"
15+
:value="value"
16+
:autocomplete="autocomplete"
17+
@input="$emit('input', $event)"
18+
/>
19+
<span class="message">{{ errorMessage }}</span>
20+
</div>
21+
<span v-if="unit" class="unit">{{ unit }}</span>
22+
</div>
23+
</template>
24+
25+
<script lang="ts">
26+
import Vue from 'vue'
27+
28+
type Rules = {
29+
[key: string]: {
30+
isValid: boolean
31+
message: string
32+
}
33+
}
34+
type SizeType = 'M' | 'S'
35+
type FontSizeType = string
36+
37+
export default Vue.extend({
38+
props: {
39+
id: {
40+
type: String,
41+
default: '',
42+
},
43+
value: {
44+
type: String,
45+
default: '',
46+
},
47+
type: {
48+
type: String,
49+
default: 'text',
50+
},
51+
name: {
52+
type: String,
53+
default: '',
54+
},
55+
placeholder: {
56+
type: String,
57+
default: '',
58+
},
59+
step: {
60+
type: Number,
61+
default: 0,
62+
},
63+
required: {
64+
type: Boolean,
65+
default: false,
66+
},
67+
spo2: {
68+
type: Boolean,
69+
default: false,
70+
},
71+
floatingPoint: {
72+
type: Boolean,
73+
default: false,
74+
},
75+
temperature: {
76+
type: Boolean,
77+
default: false,
78+
},
79+
pulse: {
80+
type: Boolean,
81+
default: false,
82+
},
83+
isNumber: {
84+
type: Boolean,
85+
default: false,
86+
},
87+
fontSize: {
88+
type: String,
89+
default: 'M',
90+
},
91+
autocomplete: {
92+
type: String,
93+
default: 'on',
94+
},
95+
unit: {
96+
type: String,
97+
default: '',
98+
},
99+
},
100+
watch: {
101+
rules() {
102+
this.showError = this.hasErrors
103+
},
104+
value() {
105+
this.$emit('validate', !this.hasErrors)
106+
},
107+
},
108+
data(): {
109+
showError: boolean
110+
fontSizeMap: Map<SizeType, FontSizeType>
111+
} {
112+
return {
113+
showError: false,
114+
fontSizeMap: new Map([
115+
['M', '20px'],
116+
['S', '16px'],
117+
]),
118+
}
119+
},
120+
computed: {
121+
rules(): Rules {
122+
return {
123+
required: {
124+
isValid: this.ruleRequired,
125+
message: '必須項目です',
126+
},
127+
floatingPoint: {
128+
isValid: this.ruleFloatingPoint,
129+
message: '小数点まで入力してください',
130+
},
131+
spo2: {
132+
isValid: this.ruleSpo2,
133+
message: '50から100までの整数で入力してください',
134+
},
135+
temperature: {
136+
isValid: this.ruleTemperature,
137+
message: '33度から45度までで入力してください',
138+
},
139+
pulse: {
140+
isValid: this.rulePulse,
141+
message: '50から400までの整数で入力してください',
142+
},
143+
isNumber: {
144+
isValid: this.ruleIsNumber,
145+
message: '負数でない整数を入力してください', // TODO: メッセージを確定させる
146+
},
147+
}
148+
},
149+
ruleRequired(): boolean {
150+
if (!this.required) return true
151+
return Boolean(this.value)
152+
},
153+
ruleFloatingPoint(): boolean {
154+
if (!this.floatingPoint) return true
155+
return this.value.match(/\d+\.\d/) != null
156+
},
157+
ruleSpo2(): boolean {
158+
if (!this.spo2) return true
159+
if (this.value.match(/[^\d]/) != null) return false
160+
return parseInt(this.value) >= 50 && parseInt(this.value) <= 100
161+
},
162+
ruleTemperature(): boolean {
163+
if (!this.temperature) return true
164+
if (this.value.match(/[^\d.]/) != null) return false
165+
return parseFloat(this.value) >= 33 && parseFloat(this.value) <= 45
166+
},
167+
rulePulse(): boolean {
168+
if (!this.pulse) return true
169+
if (this.value.match(/[^\d]/) != null) return false
170+
return parseInt(this.value) >= 50 && parseInt(this.value) <= 400
171+
},
172+
ruleIsNumber(): boolean {
173+
if (!this.isNumber) return true
174+
return this.value.match(/^\d+$/) != null
175+
},
176+
errorMessage(): string {
177+
if (!this.showError) return ''
178+
const key = Object.keys(this.rules).find(
179+
(key) => !this.rules[key].isValid,
180+
)
181+
if (!key) return ''
182+
return this.rules[key].message
183+
},
184+
hasErrors(): boolean {
185+
return Object.keys(this.rules).some(
186+
(key: string) => !this.rules[key].isValid,
187+
)
188+
},
189+
},
190+
})
191+
</script>
192+
193+
<style lang="scss" scoped>
194+
.inputFieldOuter {
195+
flex: 1 1 auto;
196+
display: flex;
197+
justify-content: space-between;
198+
align-items: flex-end;
199+
}
200+
.inputFieldControl {
201+
flex: 1 1 auto;
202+
}
203+
.inputField {
204+
width: 100%;
205+
padding: 16px;
206+
border-radius: 6px;
207+
border: 1px solid $gray-2;
208+
&:focus {
209+
outline-color: $primary;
210+
}
211+
&-error {
212+
border: 1px solid $error;
213+
&:focus {
214+
outline-color: $error;
215+
}
216+
}
217+
&::placeholder {
218+
color: $gray-2;
219+
}
220+
}
221+
.labelText {
222+
flex: 0 0 35%;
223+
font-size: 20px;
224+
}
225+
.message {
226+
display: block;
227+
color: $error;
228+
font-size: 14px;
229+
height: 14px;
230+
}
231+
.unit {
232+
font-size: 20px;
233+
padding-left: 8px;
234+
padding-bottom: 14px;
235+
}
236+
</style>

0 commit comments

Comments
 (0)