Skip to content

Commit

Permalink
feat(DsfrCheckboxSet)!: améliore l’API modelValue
Browse files Browse the repository at this point in the history
- cf. documentation

BREAKING CHANGE: `modelValue` contient un tableau des `value`
  • Loading branch information
laruiss committed Sep 15, 2024
1 parent 4af58b6 commit 0454c19
Show file tree
Hide file tree
Showing 12 changed files with 214 additions and 78 deletions.
44 changes: 42 additions & 2 deletions demo-app/views/AppForm.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
<script setup>
<script setup lang="ts">
import DsfrAlert from '@/components/DsfrAlert/DsfrAlert.vue'
import DsfrButton from '@/components/DsfrButton/DsfrButton.vue'
import DsfrCheckboxSet from '@/components/DsfrCheckbox/DsfrCheckboxSet.vue'
import DsfrFileUpload from '@/components/DsfrFileUpload/DsfrFileUpload.vue'
import DsfrRadioButtonSet from '@/components/DsfrRadioButton/DsfrRadioButtonSet.vue'
import { ref } from 'vue'
import type { DsfrCheckboxSetProps } from '@/components/DsfrCheckbox/DsfrCheckbox.types'
const inputValue = ref('')
const showAlert = ref(true)
const filesToUpload = ref(undefined)
const updateFiles = (files) => {
Expand All @@ -20,6 +22,27 @@ const sendFile = () => {
const whatever = ref('')
const radioTest = ref('')
const selectedCheckbox = ref(false)
const selectedCheckboxes = ref([])
const cbLegend = 'Légende des cases à cocher'
const cbOptions: DsfrCheckboxSetProps['options'] = [
{
value: 'test1',
modelValue: 'test1',
label: 'Test 1',
},
{
value: 'test2',
modelValue: 'test2',
label: 'Test 2',
},
{
value: 'test3',
modelValue: 'test3',
label: 'Test 3',
},
]
</script>

<template>
Expand Down Expand Up @@ -72,6 +95,7 @@ const radioTest = ref('')
</template>
</DsfrRadioButtonSet>
</div>

<DsfrButton
type="submit"
label="Bouton de soumission du formulaire"
Expand All @@ -89,5 +113,21 @@ const radioTest = ref('')
/>
</template>
</DsfrInput>

<h2>ChecboxSet :</h2>
<DsfrCheckboxSet
v-model="selectedCheckboxes"
:legend="cbLegend"
:options="cbOptions"
/>
Sélectionné(s) : {{ selectedCheckboxes }}

<h2>Checbox seule :</h2>
<DsfrCheckbox
v-model="selectedCheckbox"
label="Une seule checkbox"
value="test_only_cb"
/>
Sélectionné : {{ selectedCheckbox }}
</form>
</template>
42 changes: 42 additions & 0 deletions docs/docs-demo/DsfrCheckboxSetV7Demo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<script lang="ts" setup>
import DsfrCheckboxSet from '@/components/DsfrCheckbox/DsfrCheckboxSet.vue'
import { ref } from 'vue'
import type { DsfrCheckboxSetProps } from '@/components/DsfrCheckbox/DsfrCheckbox.types'
const selectedCheckboxes = ref([])
const cbLegend = 'Légende des cases à cocher'
const cbOptions: DsfrCheckboxSetProps['options'] = [
{
value: 'une chaîne',
modelValue: 'test1',
label: 'Test 1',
},
{
value: 42,
modelValue: 'test2',
label: 'Test 2',
},
{
value: { objet: 'complexe' },
modelValue: 'test3',
label: 'Test 3',
},
]
</script>

<template>
<form>
<DsfrCheckboxSet
v-model="selectedCheckboxes"
:legend="cbLegend"
:options="cbOptions"
/>
<p>
Sélectionné(s) : {{ selectedCheckboxes }}
</p>
<input
type="submit"
class="fr-btn"
>
</form>
</template>
19 changes: 16 additions & 3 deletions docs/guide/migrations.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
# Migrations

## Migration vers 6.x (depuis 4.x ou 5.x)
## Migration vers 7.x (depuis 4.x, 5.x ou 6.x)

Dans cette version majeure, il y a plusieurs sujets à traiter lorsque vous migrerez :
Avant la v7, le tableau `modelValue` de [`DsfrCheckboxSet`](/composants/DsfrCheckboxSet) était un tableau de `string` avec les valeurs des propriétés de l’attribut `name` de chaque case à cocher.

Ce n’était ni une API idéale, ni le comportement attendu en Vue natif ou en HTML/JS natif.

::: code-group
<Story data-title="Démo" min-h="350px">
<DsfrCheckboxSetV7Demo />
</Story>

<<< ../docs-demo/DsfrCheckboxSetV7Demo.vue [Code de la démo]
:::

1. Les icônes
2. Les onglets
3. Les accordéons

Expand Down Expand Up @@ -250,3 +259,7 @@ export default defineNuxtConfig({
],
})
```

<script setup>
import DsfrCheckboxSetV7Demo from '../docs-demo/DsfrCheckboxSetV7Demo.vue'
</script>
3 changes: 3 additions & 0 deletions src/components/DsfrCheckbox/DsfrCheckbox.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,11 @@ export const Checkbox = (args) => ({
:required="required"
:small="small"
:hint="hint"
:value="value"
:name="name || 'name1'"
v-model="modelValue"
/>
{{ modelValue }}
`,
watch: {
modelValue (newValue) {
Expand All @@ -88,6 +90,7 @@ Checkbox.args = {
small: false,
label: 'Checkbox 1',
name: 'name1',
value: 'name1',
hint: 'Description 1',
}
Checkbox.play = async ({ canvasElement }) => {
Expand Down
10 changes: 6 additions & 4 deletions src/components/DsfrCheckbox/DsfrCheckbox.types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { InputHTMLAttributes } from 'vue'

export type DsfrCheckboxProps = {
export type DsfrCheckboxProps<T = string> = {
id?: string
name: string
required?: boolean
modelValue?: boolean
value: T
checked?: boolean
modelValue: Array<T>
small?: boolean
inline?: boolean
label?: string
Expand All @@ -13,7 +15,7 @@ export type DsfrCheckboxProps = {
hint?: string
}

export type DsfrCheckboxSetProps = {
export type DsfrCheckboxSetProps<T = string> = {
titleId?: string
disabled?: boolean
inline?: boolean
Expand All @@ -23,6 +25,6 @@ export type DsfrCheckboxSetProps = {
validMessage?: string
legend?: string
options?: (DsfrCheckboxProps & InputHTMLAttributes)[]
modelValue?: string[]
modelValue?: T[]
ariaInvalid?: boolean
}
13 changes: 4 additions & 9 deletions src/components/DsfrCheckbox/DsfrCheckbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,10 @@ const props = withDefaults(defineProps<DsfrCheckboxProps>(), {
label: '',
})
const emit = defineEmits<{ (event: 'update:modelValue', value: boolean): void }>()
const message = computed(() => props.errorMessage || props.validMessage)
const additionalMessageClass = computed(() => props.errorMessage ? 'fr-error-text' : 'fr-valid-text')
const emitNewValue = ($event: InputEvent) => {
// @ts-expect-error This is a checkbox input event, so `checked` property is present
emit('update:modelValue', $event.target.checked)
}
const modelValue = defineModel()
</script>

<template>
Expand All @@ -45,14 +39,15 @@ const emitNewValue = ($event: InputEvent) => {
>
<input
:id="id"
v-model="modelValue"
:name="name"
type="checkbox"
:checked="modelValue"
:value="value"
:checked="modelValue === true || (Array.isArray(modelValue) && modelValue.includes(value))"
:required
v-bind="$attrs"
:data-testid="`input-checkbox-${id}`"
:data-test="`input-checkbox-${id}`"
@change="emitNewValue($event as InputEvent)"
>
<label
:for="id"
Expand Down
12 changes: 10 additions & 2 deletions src/components/DsfrCheckbox/DsfrCheckboxSet.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Le composant `DsfrCheckboxSet` est composé des éléments suivants :
| Nom | Type | Description | Obligatoire |
|-----------------------|-------------------------------------------|----------------------------------------------------------------|--------------|
| `options` | *`(DsfrCheckboxProps & InputHTMLAttributes)[]`* | Tableau d'options définissant les cases à cocher individuelles ||
| `modelValue` | *`string[]`* | Valeur courante du composant, un tableau de noms des cases cochées ||
| `modelValue` | *`string[]`* | Valeur courante du composant, un tableau de valeurs (propriété `value` de chaque option de la prop `options`) des cases cochées ||
| `disabled` | *`boolean`* | Indique si l'ensemble des cases à cocher est désactivé | |
| `errorMessage` | *`string`* | Message d'erreur global à afficher | |
| `inline` | *`boolean`* | Affiche les cases à cocher en ligne (par défaut : `false`) | |
Expand All @@ -26,6 +26,14 @@ Le composant `DsfrCheckboxSet` est composé des éléments suivants :
| `titleId` | *`string`* | Identifiant unique du champ (générée automatiquement si non fournie) | |
| `validMessage` | *`string`* | Message de validation global à afficher | |

::: danger Attention

Avant la v7, le tableau `modelValue` était un tableau de `string` avec les valeurs des propriétés de l’attribut `name` de chaque case à cocher.

Ce n’était ni une API idéale, ni le comportement attendu en Vue natif ou en HTML/JS natif.

:::

## 📡 Événements

`DsfrCheckboxSet` émet l'événement suivant :
Expand All @@ -43,7 +51,7 @@ Le composant `DsfrCheckboxSet` est composé des éléments suivants :

## 🪆 Relation avec `DsfrCheckbox`

`DsfrChecboxSet` utilise en interne `DsfrCheckbox`, et permet de récupérer dans `modelValue` sous forme de tableau non pas les états de chaque case à cocher, mais un tableau de `string` contenant les valeurs de la prop `name` de chaque case à cocher qui est cochée.
`DsfrChecboxSet` utilise en interne `DsfrCheckbox`, et permet de récupérer dans `modelValue` sous forme de tableau les valeurs de la prop `value` de chaque case à cocher qui est cochée.

Cf. les exemples ci-dessous

Expand Down
29 changes: 23 additions & 6 deletions src/components/DsfrCheckbox/DsfrCheckboxSet.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,30 @@ describe('DsfrCheckboxSet', () => {
const secondHintText = 'Deuxième indice'
const thirdLabelText = 'Troisième label'
const thirdHintText = 'Troisième indice'
const modelValue = []
const options = [
{
id: '1',
label: firstLabelText,
checked: false,
value: 'un',
hint: firstHintText,
modelValue,
name: '1',
},
{
id: '2',
label: secondLabelText,
checked: false,
value: 'deux',
hint: secondHintText,
modelValue,
name: '2',
},
{
id: '3',
label: thirdLabelText,
checked: false,
value: 'trois',
hint: thirdHintText,
modelValue,
name: '3',
},
]
Expand Down Expand Up @@ -64,30 +68,35 @@ describe('DsfrCheckboxSet', () => {
const secondHintText = 'Deuxième indice'
const thirdLabelText = 'Troisième label'
const thirdHintText = 'Troisième indice'
const modelValue = ['name2']

const options = [
{
id: '1',
name: 'name1',
value: 'name1',
label: firstLabelText,
hint: firstHintText,
},
{
id: '2',
name: 'name2',
value: 'name2',
label: secondLabelText,
hint: secondHintText,
},
{
id: '3',
name: 'name3',
value: 'name3',
label: thirdLabelText,
hint: thirdHintText,
},
]
const legendText = 'Légende de l’ensemble des champs'

// When
const { getByText, getByTestId } = render(DsfrCheckboxSet, {
const { getByText, getByLabelText, getByTestId } = render(DsfrCheckboxSet, {
global: {
components: {
VIcon,
Expand All @@ -96,26 +105,34 @@ describe('DsfrCheckboxSet', () => {
props: {
legend: legendText,
options,
modelValue,
validMessage: 'Message d’erreur',
modelValue: ['name3'],
},
})

const firstLabelEl = getByText(firstLabelText)
const secondLabelEl = getByText(secondLabelText)
const thirdLabelEl = getByText(`${thirdLabelText}`)
const thirdInput = getByLabelText(`${thirdLabelText} ${thirdHintText}`)
const firstInput = getByTestId('input-checkbox-1')
const secondInput = getByTestId('input-checkbox-2')
// @ts-expect-error This is a checkbox input event, so `checked` property is present
expect((firstInput).checked).toBe(false)
// @ts-expect-error This is a checkbox input event, so `checked` property is present
expect(secondInput.checked).toBe(true)
await fireEvent.click(firstLabelEl)
await fireEvent.click(secondLabelEl)
await fireEvent.click(secondLabelEl)
await fireEvent.click(thirdLabelEl)

// Then
expect(firstInput).toBeInTheDocument()
expect(firstInput).toHaveAttribute('name', 'name1')
expect(secondInput).toHaveAttribute('name', 'name2')
// @ts-expect-error This is a checkbox input event, so `checked` property is present
expect((firstInput).checked).toBe(true)
// @ts-expect-error This is a checkbox input event, so `checked` property is present
expect(secondInput.checked).toBe(false)
expect(thirdInput.checked).toBe(true)
})

it('should render no checkboxes', async () => {
Expand Down
Loading

0 comments on commit 0454c19

Please sign in to comment.