Skip to content

Commit

Permalink
Merge pull request #130 from grucloud/multi-step
Browse files Browse the repository at this point in the history
multi step form
  • Loading branch information
FredericHeem authored Nov 22, 2024
2 parents 1f79d2c + 51a275d commit 7e30992
Show file tree
Hide file tree
Showing 13 changed files with 678 additions and 122 deletions.
2 changes: 1 addition & 1 deletion bau/bau.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ type Tags = Readonly<Record<string, TagFunc<Element>>> & {
[K in keyof HTMLElementTagNameMap]: TagFunc<HTMLElementTagNameMap[K]>;
};

declare function state<T>(initVal: T): State<T>;
declare function state<T>(initVal?: T): State<T>;
declare function tagsNS(namespaceURI: string): Tags;
declare function bind(input: BindInput): HTMLElement;
declare function derive<T>(computed: () => T): ReadonlyState<T>;
Expand Down
8 changes: 6 additions & 2 deletions examples/multi-step-form/package.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
{
"name": "frontendmentor-multi-step-form",
"homepage": "https://grucloud.github.io/bau/frontendmentor/multi-step-form/",
"private": true,
"version": "0.92.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
"preview": "vite preview",
"deploy": "gh-pages -d ../../dist"
},
"devDependencies": {
"gh-pages": "6.1.1",
"typescript": "^5.0.2",
"vite": "^5.2.11"
},
"dependencies": {
"@grucloud/bau": "^0.92.0",
"@grucloud/bau-css": "^0.92.0",
"@grucloud/bau-ui": "^0.92.0"
"@grucloud/bau-ui": "^0.92.0",
"bignumber.js": "9.1.2"
}
}
17 changes: 17 additions & 0 deletions examples/multi-step-form/src/data/addons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[
{
"name": "Online service",
"description": "Access to multiplayer games",
"pricePerMonth": "1"
},
{
"name": "Larger storage",
"description": "Extra 1TB of cloud save",
"pricePerMonth": "2"
},
{
"name": "Customizable profile",
"description": "Custom theme on your profile",
"pricePerMonth": "3"
}
]
17 changes: 17 additions & 0 deletions examples/multi-step-form/src/data/plans.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[
{
"name": "Arcade",
"pricePerMonth": "9",
"image": "./assets/images/icon-arcade.svg"
},
{
"name": "Advanced",
"pricePerMonth": "12",
"image": "./assets/images/icon-advanced.svg"
},
{
"name": "Pro",
"pricePerMonth": "15",
"image": "./assets/images/icon-pro.svg"
}
]
176 changes: 131 additions & 45 deletions examples/multi-step-form/src/multiStepForm.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,67 @@
import { type Context } from "@grucloud/bau-ui/context";
import stepYourInfo from "./stepYourInfo";
import stepSelectPlan from "./stepSelectPlan";
import stepAddOns from "./stepAddOns";
import stepSummary from "./stepSummary";

import ADDONS from "./data/addons.json";
import PLANS from "./data/plans.json";

export default function (context: Context) {
const { bau, css } = context;
const { article, div, header, ul, li, span } = bau.tags;
const StepYourInfo = stepYourInfo(context);
const StepSelectPlan = stepSelectPlan(context);
const { article, div, header, ul, li } = bau.tags;

const currentStepState = bau.state(1);

const plan = bau.state<Plan>(PLANS[0]);
const isPerYear = bau.state(false);
const addons = bau.state<Addon[]>([]);

const StepYourInfo = stepYourInfo(context);
const StepSelectPlan = stepSelectPlan(context);
const StepAddOns = stepAddOns(context);
const StepSummary = stepSummary(context, { plan, isPerYear, addons });

const className = css`
border: 1px solid red;
max-width: 1000px;
margin: auto;
min-height: 500px;
background-color: var(--background-color);
display: flex;
justify-content: center;
align-items: stretch;
margin: 1rem;
gap: 0.6rem;
border-radius: 0.6rem;
padding: 1rem;
@media (max-width: 600px) {
flex-direction: column;
padding: 0rem;
margin: 0rem;
border-radius: 0px;
}
& header {
border: 1px solid blue;
background-image: url("./assets/images/bg-sidebar-desktop.svg");
background-size: cover;
flex-shrink: 0;
min-height: 10rem;
@media (max-width: 600px) {
background-image: url("./assets/images/bg-sidebar-mobile.svg");
}
> ul {
display: flex;
flex-direction: column;
@media (max-width: 600px) {
flex-direction: row;
justify-content: space-around;
}
gap: 1rem;
padding: 1rem;
padding-inline: 2rem;
padding-block: 2.4rem;
> li {
&.active {
color: red;
.step-number {
background-color: var(--pastel-blue);
color: var(--font-color);
}
}
display: flex;
align-items: center;
Expand All @@ -42,74 +71,120 @@ export default function (context: Context) {
.step-number {
font-weight: bold;
padding: 1rem;
border: 1px solid white;
border: 1px solid var(--pastel-blue);
border-radius: 100%;
width: 2rem;
height: 2rem;
display: grid;
place-content: center;
}
.step-label {
text-transform: uppercase;
font-size: smaller;
color: var(--font-color-inverse-secondary);
}
.label {
text-transform: uppercase;
font-weight: bold;
.step-labels {
@media (max-width: 600px) {
display: none;
}
.step-label {
text-transform: uppercase;
font-size: smaller;
color: var(--font-color-inverse-secondary);
font-size: 0.875rem;
}
.label {
text-transform: uppercase;
font-weight: 500;
font-size: 0.875rem;
}
}
}
}
}
.content {
border: 1px solid blue;
min-width: 375px;
min-height: 400px;
> ul {
> li {
display: none;
&.active {
display: block;
}
form {
display: flex;
flex-direction: column;
gap: 1rem;
& ul.content {
@media (max-width: 600px) {
margin-top: -5rem;
}
background-color: var(--background-color);
margin-inline: 2rem;
border-radius: 0.6rem;
> li {
display: none;
height: 100%;
&.active {
display: flex;
}
& form {
display: flex;
flex-direction: column;
gap: 1rem;
height: 100%;
padding-inline: 1.5rem;
padding-block: 2rem;
max-width: 500px;
h1 + p,
small {
color: var(--font-color-secondary);
}
}
}
}
& footer {
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
flex-grow: 1;
align-items: flex-end;
}
`;

const isCurrentIndex = (index: number) => currentStepState.val == index;

const Header = ({ index, label }: any) =>
li(
{ class: () => isCurrentIndex(index) && "active" },
div({ class: "step-number" }, index),
div(
div({ class: "step-label" }, "Step", index),
{ class: "step-labels" },
div({ class: "step-label" }, "Step ", index),
div({ class: "label" }, label)
)
);

const next = () => {
currentStepState.val++;
};
const previous = () => {
const onPrevious = () => {
currentStepState.val--;
};

const onsubmitYourInfo = (event: HTMLFormElement) => {
event.preventDefault();
const payload = Object.fromEntries(new FormData(event.currentTarget));
console.log(payload);
next();
};

const onsubmitAddon = (event) => {
const onsubmitPlan = (event: HTMLFormElement) => {
event.preventDefault();
const payload = Object.fromEntries(new FormData(event.currentTarget));
alert(JSON.stringify(payload));
// next();
const planFound = PLANS.find(({ name }) => name == String(payload.plan));
if (planFound) {
plan.val = planFound;
}
isPerYear.val = !!payload.yearly;
next();
};

const onsubmitAddon = (event: HTMLFormElement) => {
event.preventDefault();
const checkboxes = [
...event.currentTarget.querySelectorAll('input[name="addons"]:checked'),
].map(({ value }) => value);
addons.val = ADDONS.filter(({ name }) => checkboxes.includes(name));
next();
};

const onChangePlan = () => {
currentStepState.val = 2;
};

const steps = [
Expand All @@ -119,19 +194,30 @@ export default function (context: Context) {
},
{
label: "Select Plan",
Content: ({}) => StepSelectPlan({ onsubmit: onsubmitAddon }),
Content: ({}) => StepSelectPlan({ onsubmit: onsubmitPlan, onPrevious }),
},
{
label: "Add on",
Content: ({}) => StepAddOns({ onsubmit: onsubmitAddon, onPrevious }),
},
{
label: "Summary",
Content: ({}) => StepSummary({ onPrevious, onChangePlan }),
},
];

return () => {
return article(
{ class: className },
header(ul(steps.map(({ label }, index) => Header({ index, label })))),
div(
header(
ul(steps.map(({ label }, index) => Header({ index: index + 1, label })))
),
ul(
{ class: "content" },
ul(
steps.map(({ Content, label }, index) =>
li({ class: () => isCurrentIndex(index) && "active" }, Content({}))
steps.map(({ Content }, index) =>
li(
{ class: () => isCurrentIndex(index + 1) && "active" },
Content({})
)
)
)
Expand Down
Loading

0 comments on commit 7e30992

Please sign in to comment.