Skip to content

Commit 39891fc

Browse files
authored
Merge pull request #2 from opendata-swiss/showcase-form
Showcase form
2 parents 2201ff1 + 64f8533 commit 39891fc

30 files changed

+1012
-152
lines changed

opendata.swiss/ui/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,6 @@ logs
1818
.DS_Store
1919
.fleet
2020
.idea
21+
22+
# ignore images from uploads folder
23+
public/img/uploads/

opendata.swiss/ui/app/assets/main.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,7 @@
3131
.v-select {
3232
background-color: white;
3333
}
34+
35+
.form__group__input:has(> *:user-invalid), .form__group__select:has(> select:invalid) {
36+
color: #b20000;
37+
}

opendata.swiss/ui/app/components/OdsButton.vue

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
<template>
22
<button
3-
type="button"
4-
:class="['btn', 'btn--base', classes]"
5-
:aria-label="title"
6-
:title="title"
3+
:type="!submit ? 'button' : undefined"
4+
:class="['btn', 'btn--base', classes]"
5+
:aria-label="title"
6+
:title="title"
77
>
88
<slot name="icon">
9-
<SvgIcon v-if="icon" :icon="icon" :size="size" class="btn__icon" />
9+
<SvgIcon v-if="icon" :icon="icon" :size="size" class="btn__icon"/>
1010
</slot>
1111
<span class="btn__text">
12-
<a v-if="href" :href="href" >
12+
<a v-if="href" :href="href">
1313
<slot>{{ title }}</slot>
1414
</a>
1515
<slot v-else>{{ title }}</slot>
@@ -30,6 +30,7 @@ const { title, iconOnly = false, ...props } = defineProps<{
3030
iconRight?: boolean
3131
icon?: string
3232
href?: string
33+
submit?: boolean
3334
}>()
3435
3536
const classes = computed(() => {
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<template>
2+
<div class="form__group__input">
3+
<label v-if="label" :for="id" :class="labelClasses">
4+
{{ label }}<span v-if="required" class="form__group__required" />
5+
</label>
6+
<input
7+
:id="id"
8+
:type="type"
9+
:class="classes"
10+
:name="id"
11+
:placeholder="placeholder"
12+
:value="value"
13+
:min="min"
14+
:max="max"
15+
:step="step"
16+
:pattern="pattern"
17+
:autocomplete="autocomplete"
18+
:readonly="readonly"
19+
:required="required"
20+
:accept="accept"
21+
@input.stop="onInput"
22+
/>
23+
<div
24+
v-if="message"
25+
class="badge badge--sm"
26+
:class="`badge--${messageType}`"
27+
>
28+
{{ message }}
29+
</div>
30+
</div>
31+
</template>
32+
33+
<script setup lang="ts">
34+
import { computed, type PropType } from 'vue'
35+
36+
const props = defineProps({
37+
type: {
38+
type: String,
39+
validator: (prop) =>
40+
[
41+
'color',
42+
'date',
43+
'datetime-local',
44+
'email',
45+
'file',
46+
'month',
47+
'number',
48+
'password',
49+
'range',
50+
'search',
51+
'tel',
52+
'text',
53+
'time',
54+
'url',
55+
'week',
56+
'submit',
57+
].includes(prop as string),
58+
default: () => 'text',
59+
},
60+
variant: {
61+
type: String,
62+
validator: (prop) => ['outline', 'negative'].includes(prop as string),
63+
default: () => 'outline',
64+
},
65+
message: {
66+
type: String,
67+
default: () => undefined,
68+
},
69+
onInput: {
70+
type: Function as PropType<(event: Event) => void>,
71+
default: () => ({}),
72+
},
73+
messageType: {
74+
type: String,
75+
validator: (prop) =>
76+
['error', 'warning', 'success', 'info'].includes(prop as string),
77+
default: () => 'error',
78+
},
79+
size: {
80+
type: String,
81+
validator: (prop) => ['sm', 'base', 'lg'].includes(prop as string),
82+
default: () => undefined,
83+
},
84+
label: {
85+
type: String,
86+
default: () => undefined,
87+
},
88+
hideLabel: {
89+
type: Boolean,
90+
default: () => false,
91+
},
92+
placeholder: {
93+
type: String,
94+
default: () => undefined,
95+
},
96+
value: {
97+
type: String,
98+
default: () => undefined,
99+
},
100+
id: {
101+
type: String,
102+
default: () => undefined,
103+
},
104+
min: {
105+
type: Number,
106+
default: () => undefined,
107+
},
108+
max: {
109+
type: Number,
110+
default: () => undefined,
111+
},
112+
step: {
113+
type: Number,
114+
default: () => undefined,
115+
},
116+
pattern: {
117+
type: String,
118+
default: () => undefined,
119+
},
120+
autocomplete: {
121+
type: String,
122+
default: () => undefined,
123+
},
124+
readonly: {
125+
type: Boolean,
126+
default: () => false,
127+
},
128+
required: {
129+
type: Boolean,
130+
default: () => false,
131+
},
132+
accept: {
133+
type: String,
134+
default: () => undefined,
135+
}
136+
})
137+
138+
const classes = computed(() => {
139+
let base = ''
140+
if (props.variant) base += `input--${props.variant} `
141+
if (props.size) base += `input--${props.size} `
142+
if (props.message) base += `input--${props.messageType} `
143+
if (props.type === 'submit') base += 'input--submit'
144+
return base
145+
})
146+
147+
const labelClasses = computed(() => {
148+
let base = ''
149+
if (props.variant === 'negative') base += `text--negative `
150+
if (props.size) base += `text--${props.size} `
151+
if (props.hideLabel) base += `sr-only `
152+
if (props.required) base += `text--asterisk `
153+
return base
154+
})
155+
</script>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<template>
2+
<div
3+
:open="open || undefined"
4+
:class="['notification-banner', 'notification', `notification--${type}`]"
5+
:style="{
6+
transitionDuration: `${closeDuration}`,
7+
}"
8+
>
9+
<div
10+
class="notification-banner__wrapper"
11+
:style="{
12+
transitionDelay: `${closeDuration}`,
13+
}"
14+
>
15+
<p class="notification-banner__infos">
16+
<slot />
17+
</p>
18+
<slot name="buttons" />
19+
</div>
20+
</div>
21+
</template>
22+
23+
<script setup lang="ts">
24+
const { open = true, closeDuration = '0.3s' } = defineProps<{
25+
type: 'info' | 'success' | 'warning' | 'error'
26+
open?: boolean
27+
closeDuration?: string
28+
}>()
29+
</script>
30+
31+
<style scoped>
32+
.notification-banner {
33+
display: block;
34+
transition-property: padding, opacity;
35+
transition-timing-function: ease-in-out, ease-in-out;
36+
}
37+
.notification-banner:not([open]) {
38+
padding: 0;
39+
opacity: 0;
40+
overflow: hidden;
41+
}
42+
.notification-banner__wrapper {
43+
transition-property: height;
44+
transition-duration: 0s;
45+
}
46+
.notification-banner:not([open]) .notification-banner__wrapper {
47+
height: 0;
48+
}
49+
</style>
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<template>
2+
<div class="form__group__input">
3+
<label v-if="label" :for="id" :class="labelClasses">
4+
{{ label }}
5+
</label>
6+
<textarea
7+
:id="id"
8+
:class="classes"
9+
:name="id"
10+
:rows="rows"
11+
:cols="cols"
12+
:maxlength="maxlength"
13+
:minlength="minlength"
14+
:placeholder="placeholder"
15+
:required="required"
16+
/>
17+
<div
18+
v-if="message"
19+
class="badge badge--sm"
20+
:class="`badge--${messageType}`"
21+
>
22+
{{ message }}
23+
</div>
24+
</div>
25+
</template>
26+
27+
<script setup lang="ts">
28+
import { computed } from 'vue'
29+
30+
const props = defineProps({
31+
variant: {
32+
type: String,
33+
validator: (prop) => ['outline', 'negative'].includes(prop as string),
34+
default: () => undefined,
35+
},
36+
size: {
37+
type: String,
38+
validator: (prop) => ['sm', 'base', 'lg'].includes(prop as string),
39+
default: () => undefined,
40+
},
41+
id: {
42+
type: String,
43+
default: () => undefined,
44+
},
45+
name: {
46+
type: String,
47+
default: () => undefined,
48+
},
49+
label: {
50+
type: String,
51+
default: () => undefined,
52+
},
53+
placeholder: {
54+
type: String,
55+
default: () => undefined,
56+
},
57+
rows: {
58+
type: Number,
59+
default: () => 4,
60+
},
61+
cols: {
62+
type: Number,
63+
default: () => 50,
64+
},
65+
message: {
66+
type: String,
67+
default: () => undefined,
68+
},
69+
messageType: {
70+
type: String,
71+
validator: (prop) =>
72+
['error', 'warning', 'success', 'info'].includes(prop as string),
73+
default: () => undefined,
74+
},
75+
required: {
76+
type: Boolean,
77+
default: () => false,
78+
},
79+
resizable: {
80+
type: Boolean,
81+
default: () => true,
82+
},
83+
maxlength: {
84+
type: Number,
85+
default: () => undefined,
86+
},
87+
minlength: {
88+
type: Number,
89+
default: () => undefined,
90+
},
91+
})
92+
93+
const classes = computed(() => {
94+
let base = ''
95+
if (props.variant) base += `input--${props.variant} `
96+
if (props.size) base += `input--${props.size} `
97+
if (props.messageType) base += `input--${props.messageType} `
98+
if (!props.resizable) base += 'textarea--public'
99+
return base
100+
})
101+
102+
const labelClasses = computed(() => {
103+
let base = ''
104+
if (props.variant === 'negative') base += `text--negative `
105+
if (props.size) base += `text--${props.size} `
106+
if (props.required) base += `text--asterisk `
107+
return base
108+
})
109+
</script>

0 commit comments

Comments
 (0)