diff --git a/packages/ripple-ui-core/package.json b/packages/ripple-ui-core/package.json
index 943c963365..07b4e68c66 100644
--- a/packages/ripple-ui-core/package.json
+++ b/packages/ripple-ui-core/package.json
@@ -20,12 +20,18 @@
"exports": {
".": "./dist/rpl-lib.es.js",
"./nuxt": "./src/nuxt/index.ts",
+ "./components": "./src/components.ts",
"./vue": "./dist/rpl-vue.es.js",
"./webcomponents": "./dist/web-components/rpl-wc.es.js",
"./style": "./dist/global.css",
"./style/breakpoints": "./src/styles/_breakpoints.css",
"./style/components": "./dist/ripple-ui-core.css"
},
+ "typesVersions": {
+ "*": {
+ "components": ["./src/components.ts"]
+ }
+ },
"scripts": {
"build": "pnpm clean && pnpm build:types && pnpm build:tokens && pnpm build:utils && pnpm build:styles && pnpm build:lib && pnpm build:vue && pnpm build:wc",
"build:types": "tsc -p tsconfig.json",
diff --git a/packages/ripple-ui-core/src/components.ts b/packages/ripple-ui-core/src/components.ts
index 64f8fc70c2..d59c2a812a 100644
--- a/packages/ripple-ui-core/src/components.ts
+++ b/packages/ripple-ui-core/src/components.ts
@@ -45,6 +45,7 @@ export { default as RplPrimaryCampaign } from './components/campaign-banner/RplP
export { default as RplPrimaryNav } from './components/primary-nav/RplPrimaryNav.vue'
export { default as RplProfile } from './components/profile/RplProfile.vue'
export { default as RplPromoCard } from './components/card/RplPromoCard.vue'
+export { default as RplProgress } from './components/progress/RplProgress.vue'
export { default as RplRelatedLinks } from './components/related-links/RplRelatedLinks.vue'
export { default as RplResultListing } from './components/result-listing/RplResultListing.vue'
export { default as RplResultListingItem } from './components/result-listing/RplResultListingItem.vue'
diff --git a/packages/ripple-ui-forms/env.d.ts b/packages/ripple-ui-forms/env.d.ts
index e457187449..85e1eceabe 100644
--- a/packages/ripple-ui-forms/env.d.ts
+++ b/packages/ripple-ui-forms/env.d.ts
@@ -7,4 +7,16 @@ declare module '*.vue' {
export default component
}
+declare module '*.svg?component' {
+ import type { DefineComponent } from 'vue'
+ const component: DefineComponent
+ export default component
+}
+
+declare module '*.svg?skipsvgo' {
+ import type { DefineComponent } from 'vue'
+ const component: DefineComponent
+ export default component
+}
+
declare module '@dpc-sdp/ripple-ui-core/vue'
diff --git a/packages/ripple-ui-forms/src/components/RplForm/RplForm.cy.ts b/packages/ripple-ui-forms/src/components/RplForm/RplForm.cy.ts
index 72dac79899..f338b23ae5 100644
--- a/packages/ripple-ui-forms/src/components/RplForm/RplForm.cy.ts
+++ b/packages/ripple-ui-forms/src/components/RplForm/RplForm.cy.ts
@@ -1,32 +1,92 @@
import RplForm from './RplForm.vue'
-import { schema } from './fixtures/sample'
+import {
+ multiStepSchema,
+ multiStepSchemaNoNextButton,
+ multiStepSchemaNoPrevButton,
+ multiStepSchemaSubSteps,
+ schema
+} from './fixtures/sample'
-describe('', () => {
- it('renders', () => {
- // see: https://test-utils.vuejs.org/guide/
- cy.mount(RplForm, {
- props: {
- id: 'test-form',
- schema
- }
+const mountForm = (
+ formSchema: unknown,
+ extraProps: Record = {}
+) => {
+ cy.mount(RplForm, {
+ props: { id: 'test-form', schema: formSchema, ...extraProps }
+ })
+}
+
+// Helpers
+const fill = (label: string, val: string) => {
+ cy.contains('label', label)
+ .invoke('attr', 'for')
+ .then((id) => {
+ cy.get(`#${id}`).type(val)
+ })
+}
+const select = (label: string, val: string) => {
+ cy.contains('label', label)
+ .invoke('attr', 'for')
+ .then((id) => {
+ cy.get(`#${id}`).click()
+ cy.get(`#${id}__menu`).contains(val).click()
})
+}
+const check = (label: string) => cy.contains('label', label).click()
+const submit = () => cy.get('button[type="submit"]').click()
+const step = () => cy.get('.rpl-form__step:not([hidden])')
+const next = (label = 'Next') => step().contains('button', label).click()
+const back = (label = 'Back') => step().contains('button', label).click()
+
+// Assertions
+const assertStep = (heading: string, counter: string) => {
+ step().contains(heading).should('be.visible')
+ step().contains(counter).should('be.visible')
+}
+const assertError = (visible: boolean, errors: string[] = []) => {
+ cy.get('body').then(($body) => {
+ const selector =
+ $body.find('.rpl-form--multi-step').length > 0
+ ? '.rpl-form__step:not([hidden]) .rpl-form-alert'
+ : '.rpl-form-alert'
+
+ if (visible) {
+ cy.get(selector).should('be.visible')
+
+ if (errors.length) {
+ cy.get(selector).find('li').as('errors')
+ cy.get('@errors').should('have.length', errors.length)
+
+ errors.forEach((error, index) =>
+ cy.get('@errors').eq(index).should('contain', error)
+ )
+ }
+ } else {
+ cy.get(selector).should('not.exist')
+ }
+ })
+}
+
+describe('', () => {
+ beforeEach(() => {
+ cy.viewport(960, 680)
+ })
+
+ it('renders with all fields enabled', () => {
+ mountForm(schema)
cy.get('[name="name"]').should('not.be.disabled')
cy.get('[name="message"]').should('not.be.disabled')
- cy.get('[name="colour"]').should('not.have.attr', 'aria-disabled', 'true')
+ cy.get('[name="colour"]').should('not.be.disabled')
cy.get('[name="pet"]').should('not.be.disabled')
cy.get('[name="terms"]').should('not.be.disabled')
cy.get('button[type="submit"]').should('not.be.disabled')
cy.get('button[type="reset"]').should('not.be.disabled')
})
- it('form is disabled while submitting', () => {
- cy.mount(RplForm, {
- props: {
- id: 'test-form',
- schema,
- submissionState: { status: 'submitting' }
- }
+ it('disables all fields while submitting', () => {
+ mountForm(schema, {
+ submissionState: { status: 'submitting' }
})
cy.get('[name="name"]').should('be.disabled')
@@ -37,4 +97,276 @@ describe('', () => {
cy.get('button[type="submit"]').should('be.disabled')
cy.get('button[type="reset"]').should('be.disabled')
})
+
+ it('display an error message with invalid fields', () => {
+ mountForm(schema)
+
+ submit()
+
+ assertError(true, [
+ 'Name is required',
+ 'Message is required',
+ 'Colour is required',
+ 'Pet is required',
+ 'Terms is required'
+ ])
+
+ fill('Name', 'Taylor')
+ fill('Message', 'Hello')
+ select('Colour', 'Green')
+
+ submit()
+
+ assertError(true, ['Pet is required', 'Terms is required'])
+
+ check('Dog')
+ check('I accept the terms')
+
+ submit()
+
+ assertError(false)
+ })
+
+ describe('', () => {
+ it('validates and navigates between form steps', () => {
+ mountForm(multiStepSchema)
+
+ assertStep('Step one', 'Step 1 of 3')
+
+ // Attempt to advance to step two
+ next()
+ assertError(true, ['First name is required'])
+ assertStep('Step one', 'Step 1 of 3')
+
+ // Fill in the required field and advance
+ fill('First name', 'Taylor')
+ next()
+ assertError(false)
+ assertStep('Step two', 'Step 2 of 3')
+
+ // Attempt to advance to the review step
+ next('Review')
+ assertError(true, ['Email is required'])
+ assertStep('Step two', 'Step 2 of 3')
+
+ // Attempt to advance after filling the required field
+ fill('Email', 'taylor@example.com')
+ next('Review')
+ assertError(false)
+ assertStep('Review', 'Step 3 of 3')
+
+ // Attempt to submit the form
+ submit()
+ assertError(true, ['Terms is required'])
+ assertStep('Review', 'Step 3 of 3')
+
+ // And go back to the start
+ back()
+ assertStep('Step two', 'Step 2 of 3')
+
+ back()
+ assertStep('Step one', 'Step 1 of 3')
+ })
+
+ it('renders review step content', () => {
+ mountForm(multiStepSchema)
+
+ fill('First name', 'Taylor')
+ next()
+
+ fill('Email', 'taylor@example.com')
+ next('Review')
+
+ assertStep('Review', 'Step 3 of 3')
+
+ // Step one review
+ step()
+ .contains('.rpl-summary-list__title', 'Step one')
+ .parent()
+ .as('stepOne')
+
+ cy.get('@stepOne').contains('.rpl-summary-list__label', 'First name')
+ cy.get('@stepOne').contains('.rpl-summary-list__value', 'Taylor')
+ cy.get('@stepOne')
+ .contains('.rpl-summary-list__action a', 'Change')
+ .should('have.attr', 'href', '#first_name')
+
+ // Step two review
+ step()
+ .contains('.rpl-summary-list__title', 'Step two')
+ .parent()
+ .as('stepTwo')
+
+ cy.get('@stepTwo').contains('.rpl-summary-list__label', 'Email')
+ cy.get('@stepTwo').contains(
+ '.rpl-summary-list__value',
+ 'taylor@example.com'
+ )
+ cy.get('@stepTwo')
+ .contains('.rpl-summary-list__action a', 'Change')
+ .should('have.attr', 'href', '#email')
+
+ // Action link navigates back to step field
+ cy.get('@stepOne').find('a[href="#first_name"]').click()
+ cy.focused().should('have.attr', 'name', 'first_name')
+ })
+
+ it('steps can excluded from review content', () => {
+ mountForm(multiStepSchemaSubSteps)
+
+ fill('First name', 'Taylor')
+ next()
+ fill('Last name', 'Smith')
+ next()
+ fill('Email', 'taylor@example.com')
+ next()
+ check('I accept the terms')
+ next()
+
+ assertStep('Review', 'Step 3 of 3')
+
+ step()
+ .contains('.rpl-summary-list__title', 'Step one point one')
+ .should('not.exist')
+
+ step()
+ .contains('.rpl-summary-list__title', 'Step one point two')
+ .should('not.exist')
+ })
+
+ it('hides previous step button when prevButton is disabled', () => {
+ mountForm(multiStepSchemaNoPrevButton)
+
+ step().get('.rpl-form__step-prev').should('not.be.visible')
+
+ next()
+ assertStep('Step two', 'Step 2 of 3')
+ step().get('.rpl-form__step-prev').should('not.be.visible')
+
+ next('Continue')
+ step().get('.rpl-form__step-prev').should('be.visible')
+ step().get('.rpl-form__step-next').should('not.be.visible')
+ })
+
+ it('hides next step button when nextButton is disabled', () => {
+ mountForm(multiStepSchemaNoNextButton)
+
+ step().get('.rpl-form__step-prev').should('not.be.visible')
+
+ next()
+ assertStep('Step two', 'Step 2 of 3')
+ step().get('.rpl-form__step-next').should('not.be.visible')
+ step().contains('button', 'Back').should('be.visible')
+ })
+
+ it('allows navigating between steps with sub-steps', () => {
+ mountForm(multiStepSchemaSubSteps)
+
+ assertStep('Step one', 'Step 1 of 3')
+ step().get('.rpl-form__step-prev').should('not.be.visible')
+
+ next()
+ assertError(true)
+
+ fill('First name', 'Taylor')
+
+ // Go to sub-step 1.1
+ next()
+ assertStep('Step one point one', 'Step 1 of 3')
+ assertError(false)
+ step().get('.rpl-form__step-prev').should('be.visible')
+
+ next()
+ assertError(true)
+
+ fill('Last name', 'Smith')
+
+ // Go to sub-step 1.2
+ next()
+ assertStep('Step one point two', 'Step 1 of 3')
+ assertError(false)
+
+ next()
+ assertError(true)
+
+ fill('Email', 'tsmith@mail.com')
+
+ // Go to step 2
+ next()
+ assertStep('Step two', 'Step 2 of 3')
+ assertError(false)
+
+ next()
+ assertError(true)
+ assertStep('Step two', 'Step 2 of 3')
+
+ check('I accept the terms')
+
+ // Go to step 3
+ next()
+ assertStep('Review', 'Step 3 of 3')
+ assertError(false)
+
+ // Navigate back through steps
+ back()
+ assertStep('Step two', 'Step 2 of 3')
+
+ // Skips back to step 1 with custom beforeStepChange
+ back()
+ assertStep('Step one', 'Step 1 of 3')
+ step().find('[name="first_name"]').should('be.visible')
+ })
+
+ it('progress indicator does not display sub-steps', () => {
+ mountForm(multiStepSchemaSubSteps)
+
+ cy.get('.rpl-progress__title').contains('Progress')
+ cy.get('.rpl-progress__subtitle').contains('Step 1 of 3')
+ cy.get('.rpl-progress-step').as('steps')
+
+ cy.get('@steps').should('have.length', 3)
+ cy.get('@steps').eq(0).contains('Step one')
+ cy.get('@steps').eq(1).contains('Step two')
+ cy.get('@steps').eq(2).contains('Review')
+
+ cy.get('@steps').eq(0).should('have.class', 'rpl-progress-step--active')
+ cy.get('@steps')
+ .eq(1)
+ .should('not.have.class', 'rpl-progress-step--active')
+ cy.get('@steps')
+ .eq(2)
+ .should('not.have.class', 'rpl-progress-step--active')
+
+ fill('First name', 'Taylor')
+
+ // Go to sub-step 1.1
+ next()
+ cy.get('@steps').should('have.length', 3)
+ cy.get('@steps').contains('.rpl-progress-step--active', 'Step one')
+
+ fill('Last name', 'Smith')
+
+ // Go to sub-step 1.2
+ next()
+ cy.get('@steps').should('have.length', 3)
+ cy.get('@steps').contains('.rpl-progress-step--active', 'Step one')
+
+ fill('Email', 'taylor@mail.com')
+
+ // Go to step 2
+ next()
+ cy.get('@steps').should('have.length', 3)
+ cy.get('@steps').contains('.rpl-progress-step--complete', 'Step one')
+ cy.get('@steps').contains('.rpl-progress-step--active', 'Step two')
+
+ check('I accept the terms')
+
+ // Go to step 3
+ next()
+
+ cy.get('@steps').should('have.length', 3)
+ cy.get('@steps').contains('.rpl-progress-step--complete', 'Step two')
+ cy.get('@steps').contains('.rpl-progress-step--active', 'Review')
+ })
+ })
})
diff --git a/packages/ripple-ui-forms/src/components/RplForm/RplForm.vue b/packages/ripple-ui-forms/src/components/RplForm/RplForm.vue
index 6f3de3c909..e97e7afc06 100644
--- a/packages/ripple-ui-forms/src/components/RplForm/RplForm.vue
+++ b/packages/ripple-ui-forms/src/components/RplForm/RplForm.vue
@@ -421,7 +421,7 @@ const handleStepChange = async ({
// Get the current steps errors when it's invalid, and we're trying to proceed
if (!currentStep.isValid && forwards) {
- cachedErrors.value = getErrorMessages(getNode(currentStep.id))
+ cachedErrors.value = getErrorMessages(currentStep.node)
}
if (isStepValid) {
diff --git a/packages/ripple-ui-forms/src/components/RplForm/RplFormMultiStep.stories.ts b/packages/ripple-ui-forms/src/components/RplForm/RplFormMultiStep.stories.ts
new file mode 100644
index 0000000000..6036dfbb93
--- /dev/null
+++ b/packages/ripple-ui-forms/src/components/RplForm/RplFormMultiStep.stories.ts
@@ -0,0 +1,263 @@
+import type { Meta, StoryObj } from '@storybook/vue3'
+import RplForm from './RplForm.vue'
+import { handleEligibilityStepChange } from './fixtures/before-step-change'
+import '@dpc-sdp/ripple-ui-core/style/components'
+
+export default {
+ title: 'Forms/Multi step form',
+ component: RplForm
+} satisfies Meta
+
+type Story = StoryObj
+
+export const DefaultStory: Story = {
+ name: 'Default',
+ render: (args) => ({
+ components: { RplForm },
+ setup() {
+ return { args }
+ },
+ template: `
+
+
+
+
Internal form values
+
{{ value }}
+
+
+
+ `
+ }),
+ args: {
+ id: 'multi-step-form',
+ schema: [
+ {
+ $step: true,
+ id: 'eligibility',
+ key: 'eligibility',
+ name: 'eligibility',
+ title: 'Eligibility',
+ nextButton: 'Next',
+ beforeStepChange: handleEligibilityStepChange,
+ schema: [
+ {
+ $formkit: 'RplFormFieldset',
+ legend: 'Address',
+ name: 'address',
+ children: [
+ {
+ $formkit: 'RplFormText',
+ id: 'forms_address_organization',
+ name: 'organization',
+ label: 'Organization',
+ validation: [['required']],
+ validationMessages: {
+ required: 'The message field is required',
+ matches: 'Please enter between 10 and 50 characters'
+ },
+ value: ''
+ },
+ {
+ $formkit: 'RplFormText',
+ id: 'forms_address_given_name',
+ name: 'given_name',
+ label: 'Given name',
+ validation: [],
+ value: ''
+ },
+ {
+ $formkit: 'RplFormText',
+ id: 'forms_address_family_name',
+ name: 'family_name',
+ label: 'Family name',
+ validation: [],
+ value: ''
+ },
+ {
+ $formkit: 'RplFormText',
+ id: 'forms_address_address_line1',
+ name: 'address_line1',
+ label: 'Street address',
+ validation: [],
+ value: ''
+ },
+ {
+ $formkit: 'RplFormText',
+ id: 'forms_address_address_line2',
+ name: 'address_line2',
+ label: 'Street address line 2',
+ validation: [],
+ value: ''
+ },
+ {
+ $formkit: 'RplFormText',
+ id: 'forms_address_locality',
+ name: 'locality',
+ label: 'Suburb',
+ columnClasses: 'rpl-col-12 rpl-col-5-m',
+ validation: [],
+ value: ''
+ },
+ {
+ $formkit: 'RplFormDropdown',
+ id: 'forms_address_administrative_area',
+ name: 'administrative_area',
+ label: 'State',
+ columnClasses: 'rpl-col-12 rpl-col-5-m',
+ options: [
+ {
+ id: 'VIC',
+ value: 'VIC',
+ label: 'Victoria'
+ },
+ {
+ id: 'NSW',
+ value: 'NSW',
+ label: 'New South Wales'
+ },
+ {
+ id: 'WA',
+ value: 'WA',
+ label: 'Western Australia'
+ },
+ {
+ id: 'QLD',
+ value: 'QLD',
+ label: 'Queensland'
+ },
+ {
+ id: 'ACT',
+ value: 'ACT',
+ label: 'Australian Capital Territory'
+ },
+ {
+ id: 'NT',
+ value: 'NT',
+ label: 'Northern Territory'
+ },
+ {
+ id: 'SA',
+ value: 'SA',
+ label: 'South Australia'
+ },
+ {
+ id: 'TAS',
+ value: 'TAS',
+ label: 'Tasmania'
+ }
+ ],
+ validation: [],
+ value: '',
+ pii: false
+ },
+ {
+ $formkit: 'RplFormText',
+ id: 'forms_address_postal_code',
+ name: 'postal_code',
+ label: 'Postcode',
+ columnClasses: 'rpl-col-6 rpl-col-3-m',
+ validation: [],
+ value: ''
+ },
+ {
+ $formkit: 'hidden',
+ id: 'forms_address_country_code',
+ name: 'country_code',
+ value: 'AU'
+ }
+ ]
+ }
+ ]
+ },
+ {
+ $step: true,
+ id: 'not-eligible',
+ key: 'not-eligible',
+ name: 'not-eligible',
+ title: 'Not eligible',
+ nextButton: false,
+ prevButton: 'Go backwards',
+ beforeStepChange: handleEligibilityStepChange,
+ parentStep: 'eligibility',
+ schema: [
+ {
+ $el: 'div',
+ attrs: {
+ class: 'rpl-content'
+ },
+ children: [
+ {
+ $el: 'p',
+ children:
+ 'Unfortunately, based on the information you have provided, you are not eligible to apply for this grant.'
+ }
+ ]
+ }
+ ]
+ },
+ {
+ $step: true,
+ id: 'eligible',
+ key: 'eligible',
+ name: 'eligible',
+ title: 'You are eligible',
+ nextButton: 'Go forwards',
+ prevButton: 'Go backwards',
+ beforeStepChange: handleEligibilityStepChange,
+ parentStep: 'eligibility',
+ schema: [
+ {
+ $el: 'div',
+ attrs: {
+ class: ['rpl-content', 'rpl-u-padding-b-4']
+ },
+ children: [
+ {
+ $el: 'p',
+ children:
+ 'Congratulations! Based on the information you have provided, you are eligible to apply for this grant.'
+ }
+ ]
+ }
+ ]
+ },
+ {
+ $step: true,
+ id: 'review',
+ key: 'review',
+ name: 'review',
+ title: 'Review',
+ nextButton: 'Submit',
+ prevButton: 'Go backwards',
+ beforeStepChange: handleEligibilityStepChange,
+ schema: [
+ {
+ $formkit: 'RplFormReview',
+ key: 'review_component'
+ },
+ {
+ $formkit: 'RplFormCheckbox',
+ id: 'terms',
+ name: 'terms',
+ label: 'Terms',
+ checkboxLabel: 'I accept the terms',
+ validation: [['required']]
+ },
+ {
+ $formkit: 'RplFormActions',
+ name: 'submit',
+ variant: 'filled',
+ label: 'Submit',
+ id: 'actions',
+ displayResetButton: true
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/packages/ripple-ui-forms/src/components/RplForm/fixtures/before-step-change.ts b/packages/ripple-ui-forms/src/components/RplForm/fixtures/before-step-change.ts
new file mode 100644
index 0000000000..f2e841b7af
--- /dev/null
+++ b/packages/ripple-ui-forms/src/components/RplForm/fixtures/before-step-change.ts
@@ -0,0 +1,57 @@
+import type { StepChangeData } from '@formkit/addons'
+export const handleEligibilityStepChange = async ({
+ currentStep,
+ targetStep,
+ delta
+}: StepChangeData) => {
+ // Delta < 0 indicates navigating backwards.
+ // We want to allow users to navigate back to the eligibility step, but prevent navigating back to the branch steps once they've moved past them.
+ if (delta < 0) {
+ const branchSteps = ['eligible', 'not-eligible']
+ const isCurrentBranchStep = branchSteps.includes(currentStep.node.name)
+ const isTargetBranchStep = branchSteps.includes(targetStep.node.name)
+
+ // Allow branch step -> eligibility directly (no interception).
+ if (isCurrentBranchStep && targetStep.node.name === 'eligibility') {
+ return true
+ }
+
+ // Prevent navigating backwards between branch steps.
+ if (isCurrentBranchStep && isTargetBranchStep) {
+ currentStep.node.parent?.goTo('eligibility')
+ return false
+ }
+
+ // Prevent review from going back to a branch step directly.
+ if (currentStep.node.name === 'review' && isTargetBranchStep) {
+ currentStep.node.parent?.goTo('eligibility')
+ return false
+ }
+
+ return true
+ }
+
+ if (currentStep.node.name === 'eligibility') {
+ const value = currentStep.value ?? {}
+ const address = (value as any).address ?? {}
+ const organization = address.organization
+
+ const desiredStep = organization === 'test' ? 'eligible' : 'not-eligible'
+
+ if (desiredStep !== targetStep.node.name) {
+ currentStep.node.parent?.goTo(desiredStep)
+ return false
+ }
+ }
+
+ if (currentStep.node.name === 'eligible') {
+ return true
+ }
+
+ if (currentStep.node.name === 'not-eligible') {
+ currentStep.node.parent?.goTo('eligibility')
+ return false
+ }
+
+ return true
+}
diff --git a/packages/ripple-ui-forms/src/components/RplForm/fixtures/sample.ts b/packages/ripple-ui-forms/src/components/RplForm/fixtures/sample.ts
index bb589f63e1..bd11cc86cd 100644
--- a/packages/ripple-ui-forms/src/components/RplForm/fixtures/sample.ts
+++ b/packages/ripple-ui-forms/src/components/RplForm/fixtures/sample.ts
@@ -3,13 +3,15 @@ export const schema = [
$formkit: 'RplFormText',
name: 'name',
label: 'Name',
- id: 'name'
+ id: 'name',
+ validation: [['required']]
},
{
$formkit: 'RplFormTextarea',
name: 'message',
label: 'Message',
- id: 'message'
+ id: 'message',
+ validation: [['required']]
},
{
$formkit: 'RplFormDropdown',
@@ -27,7 +29,8 @@ export const schema = [
value: 'Blue',
label: 'Blue'
}
- ]
+ ],
+ validation: [['required']]
},
{
$formkit: 'RplFormRadioGroup',
@@ -45,14 +48,16 @@ export const schema = [
value: 'cat',
label: 'Cat'
}
- ]
+ ],
+ validation: [['required']]
},
{
$formkit: 'RplFormCheckbox',
id: 'terms',
name: 'terms',
label: 'Terms',
- checkboxLabel: 'I accept the terms'
+ checkboxLabel: 'I accept the terms',
+ validation: [['required']]
},
{
$formkit: 'RplFormActions',
@@ -63,3 +68,280 @@ export const schema = [
displayResetButton: true
}
]
+
+export const multiStepSchema: any[] = [
+ {
+ $step: true,
+ id: 'step-one',
+ key: 'step-one',
+ name: 'step-one',
+ title: 'Step one',
+ nextButton: 'Next',
+ schema: [
+ {
+ $formkit: 'RplFormText',
+ id: 'first_name',
+ name: 'first_name',
+ label: 'First name',
+ validation: [['required']],
+ value: ''
+ }
+ ]
+ },
+ {
+ $step: true,
+ id: 'step-two',
+ key: 'step-two',
+ name: 'step-two',
+ title: 'Step two',
+ nextButton: 'Review',
+ prevButton: 'Back',
+ schema: [
+ {
+ $formkit: 'RplFormText',
+ id: 'email',
+ name: 'email',
+ label: 'Email',
+ validation: [['required']],
+ value: ''
+ }
+ ]
+ },
+ {
+ $step: true,
+ id: 'review',
+ key: 'review',
+ name: 'review',
+ title: 'Review',
+ nextButton: 'Submit',
+ prevButton: 'Back',
+ schema: [
+ {
+ $formkit: 'RplFormReview',
+ key: 'review_component'
+ },
+ {
+ $formkit: 'RplFormCheckbox',
+ id: 'terms',
+ name: 'terms',
+ label: 'Terms',
+ checkboxLabel: 'I accept the terms',
+ validation: [['required']]
+ },
+ {
+ $formkit: 'RplFormActions',
+ name: 'submit',
+ variant: 'filled',
+ label: 'Submit',
+ id: 'actions',
+ displayResetButton: true
+ }
+ ]
+ }
+]
+
+export const multiStepSchemaSubSteps: any[] = [
+ {
+ $step: true,
+ id: 'step-one',
+ key: 'step-one',
+ name: 'step-one',
+ title: 'Step one',
+ nextButton: 'Next',
+ schema: [
+ {
+ $formkit: 'RplFormText',
+ id: 'first_name',
+ name: 'first_name',
+ label: 'First name',
+ validation: [['required']],
+ value: ''
+ }
+ ]
+ },
+ {
+ $step: true,
+ parentStep: 'step-one',
+ inReview: false,
+ id: 'step-one-point-one',
+ key: 'step-one-point-one',
+ name: 'step-one-point-one',
+ title: 'Step one point one',
+ nextButton: 'Next',
+ schema: [
+ {
+ $formkit: 'RplFormText',
+ id: 'last_name',
+ name: 'last_name',
+ label: 'Last name',
+ validation: [['required']],
+ value: ''
+ }
+ ]
+ },
+ {
+ $step: true,
+ parentStep: 'step-one',
+ inReview: false,
+ id: 'step-one-point-two',
+ key: 'step-one-point-two',
+ name: 'step-one-point-two',
+ title: 'Step one point two',
+ nextButton: 'Next',
+ schema: [
+ {
+ $formkit: 'RplFormEmail',
+ id: 'email',
+ name: 'email',
+ label: 'Email',
+ validation: [['required']],
+ value: ''
+ }
+ ]
+ },
+ {
+ $step: true,
+ id: 'step-two',
+ key: 'step-two',
+ name: 'step-two',
+ title: 'Step two',
+ nextButton: 'Next',
+ beforeStepChange: async ({ currentStep, targetStep }) => {
+ if (targetStep.id === 'step-one-point-two') {
+ currentStep.node.parent?.goTo('step-one')
+ }
+ return true
+ },
+ schema: [
+ {
+ $formkit: 'RplFormCheckbox',
+ id: 'terms',
+ name: 'terms',
+ label: 'Terms',
+ checkboxLabel: 'I accept the terms',
+ validation: [['required']]
+ }
+ ]
+ },
+ {
+ $step: true,
+ id: 'review',
+ key: 'review',
+ name: 'review',
+ title: 'Review',
+ nextButton: 'Submit',
+ prevButton: 'Back',
+ schema: [
+ {
+ $formkit: 'RplFormReview',
+ key: 'review_component'
+ }
+ ]
+ }
+]
+
+export const multiStepSchemaNoPrevButton: any[] = [
+ {
+ $step: true,
+ id: 'step-one',
+ key: 'step-one',
+ name: 'step-one',
+ title: 'Step one',
+ nextButton: 'Next',
+ schema: [
+ {
+ $formkit: 'RplFormText',
+ id: 'first_name',
+ name: 'first_name',
+ label: 'First name',
+ value: ''
+ }
+ ]
+ },
+ {
+ $step: true,
+ id: 'step-two',
+ key: 'step-two',
+ name: 'step-two',
+ title: 'Step two',
+ nextButton: 'Continue',
+ prevButton: false,
+ schema: [
+ {
+ $formkit: 'RplFormText',
+ id: 'email',
+ name: 'email',
+ label: 'Email',
+ value: ''
+ }
+ ]
+ },
+ {
+ $step: true,
+ id: 'review',
+ key: 'review',
+ name: 'review',
+ title: 'Review',
+ nextButton: 'Submit',
+ prevButton: 'Back',
+ schema: [
+ {
+ $formkit: 'RplFormReview',
+ key: 'review_component'
+ }
+ ]
+ }
+]
+
+export const multiStepSchemaNoNextButton: any[] = [
+ {
+ $step: true,
+ id: 'step-one',
+ key: 'step-one',
+ name: 'step-one',
+ title: 'Step one',
+ nextButton: 'Next',
+ schema: [
+ {
+ $formkit: 'RplFormText',
+ id: 'first_name_no_next',
+ name: 'first_name_no_next',
+ label: 'First name',
+ value: ''
+ }
+ ]
+ },
+ {
+ $step: true,
+ id: 'step-two',
+ key: 'step-two',
+ name: 'step-two',
+ title: 'Step two',
+ nextButton: false,
+ prevButton: 'Back',
+ schema: [
+ {
+ $formkit: 'RplFormText',
+ id: 'email_no_next',
+ name: 'email_no_next',
+ label: 'Email',
+ value: ''
+ }
+ ]
+ },
+ {
+ $step: true,
+ id: 'review',
+ key: 'review',
+ name: 'review',
+ title: 'Review',
+ nextButton: 'Submit',
+ prevButton: 'Back',
+ schema: [
+ {
+ $formkit: 'RplFormReview',
+ key: 'review_component'
+ }
+ ]
+ }
+]
diff --git a/packages/ripple-ui-forms/src/components/RplFormReview/RplFormReview.vue b/packages/ripple-ui-forms/src/components/RplFormReview/RplFormReview.vue
index 99f3bd7804..0ac6aa7b77 100644
--- a/packages/ripple-ui-forms/src/components/RplFormReview/RplFormReview.vue
+++ b/packages/ripple-ui-forms/src/components/RplFormReview/RplFormReview.vue
@@ -3,6 +3,7 @@ import { computed, inject } from 'vue'
import { FormKitSchemaNode } from '@formkit/core'
import { isValid, parse } from 'date-fns'
import { formatDate } from '@dpc-sdp/ripple-ui-core'
+import { RplSummaryList } from '@dpc-sdp/ripple-ui-core/components'
import { IRplFormProvidedState, RplFormKitStepNode } from '../../types'
interface Props {
@@ -102,7 +103,7 @@ const items = computed(() => {
values = form.stepsId ? values[form.stepsId] || values : values
const stepItems = (form?.schema || []).filter(
- (item) => (item).$step
+ (item: RplFormKitStepNode) => item.$step && item.inReview !== false
) as RplFormKitStepNode[]
const stepItemsProcessed = stepItems.map((step) => {
diff --git a/packages/ripple-ui-forms/src/components/RplFormStep/RplFormStep.vue b/packages/ripple-ui-forms/src/components/RplFormStep/RplFormStep.vue
index 50fcbec795..8a092b2f4a 100644
--- a/packages/ripple-ui-forms/src/components/RplFormStep/RplFormStep.vue
+++ b/packages/ripple-ui-forms/src/components/RplFormStep/RplFormStep.vue
@@ -7,9 +7,11 @@
:input-errors="inputErrors"
:previous-label="prevButton"
:next-label="nextButton"
+ :before-step-change="beforeStepChange"
+ @node="setStepNode"
>