Repository: lldap/lldap (language: rust)

Issue:
[CLEANUP] Use new form components across all components
New form components were added in #802 to simplify the html of forms. Where applicable, switch old form inputs over to the new components.


Discussion / hints:

Fix patch:
diff --git a/app/src/components/change_password.rs b/app/src/components/change_password.rs
index 296fd9dc..642fb1a7 100644
--- a/app/src/components/change_password.rs
+++ b/app/src/components/change_password.rs
@@ -1,5 +1,8 @@
 use crate::{
-    components::router::{AppRoute, Link},
+    components::{
+        form::{field::Field, submit::Submit},
+        router::{AppRoute, Link},
+    },
     infra::{
         api::HostService,
         common_component::{CommonComponent, CommonComponentParts},
@@ -207,7 +210,6 @@ impl Component for ChangePasswordForm {
     fn view(&self, ctx: &Context<Self>) -> Html {
         let is_admin = ctx.props().is_admin;
         let link = ctx.link();
-        type Field = yew_form::Field<FormModel>;
         html! {
           <>
             <div class="mb-2 mt-2">
@@ -224,90 +226,44 @@ impl Component for ChangePasswordForm {
                 }
               } else { html! {} }
             }
-            <form
-              class="form">
+            <form class="form">
               {if !is_admin { html! {
-                <div class="form-group row">
-                  <label for="old_password"
-                    class="form-label col-sm-2 col-form-label">
-                    {"Current password*:"}
-                  </label>
-                  <div class="col-sm-10">
-                    <Field
-                      form={&self.form}
-                      field_name="old_password"
-                      input_type="password"
-                      class="form-control"
-                      class_invalid="is-invalid has-error"
-                      class_valid="has-success"
-                      autocomplete="current-password"
-                      oninput={link.callback(|_| Msg::FormUpdate)} />
-                    <div class="invalid-feedback">
-                      {&self.form.field_message("old_password")}
-                    </div>
-                  </div>
-                </div>
+                <Field<FormModel>
+                  form={&self.form}
+                  required=true
+                  label="Current password"
+                  field_name="old_password"
+                  input_type="password"
+                  autocomplete="current-password"
+                  oninput={link.callback(|_| Msg::FormUpdate)} />
               }} else { html! {} }}
-              <div class="form-group row mb-3">
-                <label for="new_password"
-                  class="form-label col-sm-2 col-form-label">
-                   {"New Password"}
-                   <span class="text-danger">{"*"}</span>
-                   {":"}
-                </label>
-                <div class="col-sm-10">
-                  <Field
-                    form={&self.form}
-                    field_name="password"
-                    input_type="password"
-                    class="form-control"
-                    class_invalid="is-invalid has-error"
-                    class_valid="has-success"
-                    autocomplete="new-password"
-                    oninput={link.callback(|_| Msg::FormUpdate)} />
-                  <div class="invalid-feedback">
-                    {&self.form.field_message("password")}
-                  </div>
-                </div>
-              </div>
-              <div class="form-group row mb-3">
-                <label for="confirm_password"
-                  class="form-label col-sm-2 col-form-label">
-                  {"Confirm Password"}
-                  <span class="text-danger">{"*"}</span>
-                  {":"}
-                </label>
-                <div class="col-sm-10">
-                  <Field
-                    form={&self.form}
-                    field_name="confirm_password"
-                    input_type="password"
-                    class="form-control"
-                    class_invalid="is-invalid has-error"
-                    class_valid="has-success"
-                    autocomplete="new-password"
-                    oninput={link.callback(|_| Msg::FormUpdate)} />
-                  <div class="invalid-feedback">
-                    {&self.form.field_message("confirm_password")}
-                  </div>
-                </div>
-              </div>
-              <div class="form-group row justify-content-center">
-                <button
-                  class="btn btn-primary col-auto col-form-label"
-                  type="submit"
-                  disabled={self.common.is_task_running()}
-                  onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})}>
-                  <i class="bi-save me-2"></i>
-                  {"Save changes"}
-                </button>
+              <Field<FormModel>
+                form={&self.form}
+                required=true
+                label="New password"
+                field_name="password"
+                input_type="password"
+                autocomplete="new-password"
+                oninput={link.callback(|_| Msg::FormUpdate)} />
+              <Field<FormModel>
+                form={&self.form}
+                required=true
+                label="Confirm password"
+                field_name="confirm_password"
+                input_type="password"
+                autocomplete="new-password"
+                oninput={link.callback(|_| Msg::FormUpdate)} />
+              <Submit
+                disabled={self.common.is_task_running()}
+                onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})}
+                text="Save changes" >
                 <Link
                   classes="btn btn-secondary ms-2 col-auto col-form-label"
                   to={AppRoute::UserDetails{user_id: ctx.props().username.clone()}}>
                   <i class="bi-arrow-return-left me-2"></i>
                   {"Back"}
                 </Link>
-              </div>
+              </Submit>
             </form>
           </>
         }
diff --git a/app/src/components/create_group.rs b/app/src/components/create_group.rs
index a7e66412..32820071 100644
--- a/app/src/components/create_group.rs
+++ b/app/src/components/create_group.rs
@@ -1,5 +1,8 @@
 use crate::{
-    components::router::AppRoute,
+    components::{
+        form::{field::Field, submit::Submit},
+        router::AppRoute,
+    },
     infra::common_component::{CommonComponent, CommonComponentParts},
 };
 use anyhow::{bail, Result};
@@ -93,44 +96,21 @@ impl Component for CreateGroupForm {
 
     fn view(&self, ctx: &Context<Self>) -> Html {
         let link = ctx.link();
-        type Field = yew_form::Field<CreateGroupModel>;
         html! {
           <div class="row justify-content-center">
             <form class="form py-3" style="max-width: 636px">
               <div class="row mb-3">
                 <h5 class="fw-bold">{"Create a group"}</h5>
               </div>
-              <div class="form-group row mb-3">
-                <label for="groupname"
-                  class="form-label col-4 col-form-label">
-                  {"Group name"}
-                  <span class="text-danger">{"*"}</span>
-                  {":"}
-                </label>
-                <div class="col-8">
-                  <Field
-                    form={&self.form}
-                    field_name="groupname"
-                    class="form-control"
-                    class_invalid="is-invalid has-error"
-                    class_valid="has-success"
-                    autocomplete="groupname"
-                    oninput={link.callback(|_| Msg::Update)} />
-                  <div class="invalid-feedback">
-                    {&self.form.field_message("groupname")}
-                  </div>
-                </div>
-              </div>
-              <div class="form-group row justify-content-center">
-                <button
-                  class="btn btn-primary col-auto col-form-label"
-                  type="submit"
-                  disabled={self.common.is_task_running()}
-                  onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitForm})}>
-                  <i class="bi-save me-2"></i>
-                  {"Submit"}
-                </button>
-              </div>
+              <Field<CreateGroupModel>
+                form={&self.form}
+                required=true
+                label="Group name"
+                field_name="groupname"
+                oninput={link.callback(|_| Msg::Update)} />
+              <Submit
+                disabled={self.common.is_task_running()}
+                onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitForm})} />
             </form>
             { if let Some(e) = &self.common.error {
                 html! {
diff --git a/app/src/components/create_user.rs b/app/src/components/create_user.rs
index c8c89392..a5cca696 100644
--- a/app/src/components/create_user.rs
+++ b/app/src/components/create_user.rs
@@ -1,5 +1,8 @@
 use crate::{
-    components::router::AppRoute,
+    components::{
+        form::{field::Field, submit::Submit},
+        router::AppRoute,
+    },
     infra::{
         api::HostService,
         common_component::{CommonComponent, CommonComponentParts},
@@ -187,163 +190,57 @@ impl Component for CreateUserForm {
 
     fn view(&self, ctx: &Context<Self>) -> Html {
         let link = &ctx.link();
-        type Field = yew_form::Field<CreateUserModel>;
         html! {
           <div class="row justify-content-center">
             <form class="form py-3" style="max-width: 636px">
-              <div class="row mb-3">
-                <h5 class="fw-bold">{"Create a user"}</h5>
-              </div>
-              <div class="form-group row mb-3">
-                <label for="username"
-                  class="form-label col-4 col-form-label">
-                  {"User name"}
-                  <span class="text-danger">{"*"}</span>
-                  {":"}
-                </label>
-                <div class="col-8">
-                  <Field
-                    form={&self.form}
-                    field_name="username"
-                    class="form-control"
-                    class_invalid="is-invalid has-error"
-                    class_valid="has-success"
-                    autocomplete="username"
-                    oninput={link.callback(|_| Msg::Update)} />
-                  <div class="invalid-feedback">
-                    {&self.form.field_message("username")}
-                  </div>
-                </div>
-              </div>
-              <div class="form-group row mb-3">
-                <label for="email"
-                  class="form-label col-4 col-form-label">
-                  {"Email"}
-                  <span class="text-danger">{"*"}</span>
-                  {":"}
-                </label>
-                <div class="col-8">
-                  <Field
-                    form={&self.form}
-                    input_type="email"
-                    field_name="email"
-                    class="form-control"
-                    class_invalid="is-invalid has-error"
-                    class_valid="has-success"
-                    autocomplete="email"
-                    oninput={link.callback(|_| Msg::Update)} />
-                  <div class="invalid-feedback">
-                    {&self.form.field_message("email")}
-                  </div>
-                </div>
-              </div>
-              <div class="form-group row mb-3">
-                <label for="display_name"
-                  class="form-label col-4 col-form-label">
-                  {"Display name:"}
-                </label>
-                <div class="col-8">
-                  <Field
-                    form={&self.form}
-                    autocomplete="name"
-                    class="form-control"
-                    class_invalid="is-invalid has-error"
-                    class_valid="has-success"
-                    field_name="display_name"
-                    oninput={link.callback(|_| Msg::Update)} />
-                  <div class="invalid-feedback">
-                    {&self.form.field_message("display_name")}
-                  </div>
-                </div>
-              </div>
-              <div class="form-group row mb-3">
-                <label for="first_name"
-                  class="form-label col-4 col-form-label">
-                  {"First name:"}
-                </label>
-                <div class="col-8">
-                  <Field
-                    form={&self.form}
-                    autocomplete="given-name"
-                    class="form-control"
-                    class_invalid="is-invalid has-error"
-                    class_valid="has-success"
-                    field_name="first_name"
-                    oninput={link.callback(|_| Msg::Update)} />
-                  <div class="invalid-feedback">
-                    {&self.form.field_message("first_name")}
-                  </div>
-                </div>
-              </div>
-              <div class="form-group row mb-3">
-                <label for="last_name"
-                  class="form-label col-4 col-form-label">
-                  {"Last name:"}
-                </label>
-                <div class="col-8">
-                  <Field
-                    form={&self.form}
-                    autocomplete="family-name"
-                    class="form-control"
-                    class_invalid="is-invalid has-error"
-                    class_valid="has-success"
-                    field_name="last_name"
-                    oninput={link.callback(|_| Msg::Update)} />
-                  <div class="invalid-feedback">
-                    {&self.form.field_message("last_name")}
-                  </div>
-                </div>
-              </div>
-              <div class="form-group row mb-3">
-                <label for="password"
-                  class="form-label col-4 col-form-label">
-                  {"Password:"}
-                </label>
-                <div class="col-8">
-                  <Field
-                    form={&self.form}
-                    input_type="password"
-                    field_name="password"
-                    class="form-control"
-                    class_invalid="is-invalid has-error"
-                    class_valid="has-success"
-                    autocomplete="new-password"
-                    oninput={link.callback(|_| Msg::Update)} />
-                  <div class="invalid-feedback">
-                    {&self.form.field_message("password")}
-                  </div>
-                </div>
-              </div>
-              <div class="form-group row mb-3">
-                <label for="confirm_password"
-                  class="form-label col-4 col-form-label">
-                  {"Confirm password:"}
-                </label>
-                <div class="col-8">
-                  <Field
-                    form={&self.form}
-                    input_type="password"
-                    field_name="confirm_password"
-                    class="form-control"
-                    class_invalid="is-invalid has-error"
-                    class_valid="has-success"
-                    autocomplete="new-password"
-                    oninput={link.callback(|_| Msg::Update)} />
-                  <div class="invalid-feedback">
-                    {&self.form.field_message("confirm_password")}
-                  </div>
-                </div>
-              </div>
-              <div class="form-group row justify-content-center">
-                <button
-                  class="btn btn-primary col-auto col-form-label mt-4"
-                  disabled={self.common.is_task_running()}
-                  type="submit"
-                  onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitForm})}>
-                  <i class="bi-save me-2"></i>
-                  {"Submit"}
-                </button>
-              </div>
+              <Field<CreateUserModel>
+                form={&self.form}
+                required=true
+                label="User name"
+                field_name="username"
+                oninput={link.callback(|_| Msg::Update)} />
+              <Field<CreateUserModel>
+                form={&self.form}
+                required=true
+                label="Email"
+                field_name="email"
+                input_type="email"
+                oninput={link.callback(|_| Msg::Update)} />
+              <Field<CreateUserModel>
+                form={&self.form}
+                label="Display name"
+                field_name="display_name"
+                autocomplete="name"
+                oninput={link.callback(|_| Msg::Update)} />
+              <Field<CreateUserModel>
+                form={&self.form}
+                label="First name"
+                field_name="first_name"
+                autocomplete="given-name"
+                oninput={link.callback(|_| Msg::Update)} />
+              <Field<CreateUserModel>
+                form={&self.form}
+                label="Last name"
+                field_name="last_name"
+                autocomplete="family-name"
+                oninput={link.callback(|_| Msg::Update)} />
+              <Field<CreateUserModel>
+                form={&self.form}
+                label="Password"
+                field_name="password"
+                input_type="password"
+                autocomplete="new-password"
+                oninput={link.callback(|_| Msg::Update)} />
+              <Field<CreateUserModel>
+                form={&self.form}
+                label="Confirm password"
+                field_name="confirm_password"
+                input_type="password"
+                autocomplete="new-password"
+                oninput={link.callback(|_| Msg::Update)} />
+              <Submit
+                disabled={self.common.is_task_running()}
+                onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitForm})} />
             </form>
             {
               if let Some(e) = &self.common.error {
diff --git a/app/src/components/form/field.rs b/app/src/components/form/field.rs
index ab5018fa..7ae385f8 100644
--- a/app/src/components/form/field.rs
+++ b/app/src/components/form/field.rs
@@ -8,6 +8,11 @@ pub struct Props<T: Model> {
     pub form: Form<T>,
     #[prop_or(false)]
     pub required: bool,
+    #[prop_or(String::from("text"))]
+    pub input_type: String,
+    // If not present, will default to field_name
+    #[prop_or(None)]
+    pub autocomplete: Option<String>,
     #[prop_or_else(Callback::noop)]
     pub oninput: Callback<InputEvent>,
 }
@@ -15,28 +20,29 @@ pub struct Props<T: Model> {
 #[function_component(Field)]
 pub fn field<T: Model>(props: &Props<T>) -> Html {
     html! {
-        <div class="row mb-3">
-            <label for={props.field_name.clone()}
-                class="form-label col-4 col-form-label">
-                {&props.label}
-                {if props.required {
-                    html!{<span class="text-danger">{"*"}</span>}
-                } else {html!{}}}
-                {":"}
-            </label>
-            <div class="col-8">
-                <yew_form::Field<T>
-                    form={&props.form}
-                    field_name={props.field_name.clone()}
-                    class="form-control"
-                    class_invalid="is-invalid has-error"
-                    class_valid="has-success"
-                    autocomplete={props.field_name.clone()}
-                    oninput={&props.oninput} />
-                    <div class="invalid-feedback">
-                        {&props.form.field_message(&props.field_name)}
-                    </div>
+      <div class="row mb-3">
+        <label for={props.field_name.clone()}
+          class="form-label col-4 col-form-label">
+          {&props.label}
+          {if props.required {
+            html!{<span class="text-danger">{"*"}</span>}
+          } else {html!{}}}
+          {":"}
+        </label>
+        <div class="col-8">
+          <yew_form::Field<T>
+            form={&props.form}
+            field_name={props.field_name.clone()}
+            input_type={props.input_type.clone()}
+            class="form-control"
+            class_invalid="is-invalid has-error"
+            class_valid="has-success"
+            autocomplete={props.autocomplete.clone().unwrap_or(props.field_name.clone())}
+            oninput={&props.oninput} />
+            <div class="invalid-feedback">
+              {&props.form.field_message(&props.field_name)}
             </div>
         </div>
+      </div>
     }
 }
diff --git a/app/src/components/form/mod.rs b/app/src/components/form/mod.rs
index dc112e38..f46bdede 100644
--- a/app/src/components/form/mod.rs
+++ b/app/src/components/form/mod.rs
@@ -1,4 +1,5 @@
 pub mod checkbox;
 pub mod field;
 pub mod select;
+pub mod static_value;
 pub mod submit;
diff --git a/app/src/components/form/static_value.rs b/app/src/components/form/static_value.rs
new file mode 100644
index 00000000..85874cc1
--- /dev/null
+++ b/app/src/components/form/static_value.rs
@@ -0,0 +1,26 @@
+use yew::{function_component, html, virtual_dom::AttrValue, Children, Properties};
+
+#[derive(Properties, PartialEq)]
+pub struct Props {
+    pub label: AttrValue,
+    pub id: AttrValue,
+    pub children: Children,
+}
+
+#[function_component(StaticValue)]
+pub fn static_value(props: &Props) -> Html {
+    html! {
+      <div class="row mb-3">
+        <label for={props.id.clone()}
+          class="form-label col-4 col-form-label">
+          {&props.label}
+          {":"}
+        </label>
+        <div class="col-8">
+          <span id={props.id.clone()} class="form-control-static">
+            {for props.children.iter()}
+          </span>
+        </div>
+      </div>
+    }
+}
diff --git a/app/src/components/form/submit.rs b/app/src/components/form/submit.rs
index 622917f9..29114401 100644
--- a/app/src/components/form/submit.rs
+++ b/app/src/components/form/submit.rs
@@ -1,24 +1,30 @@
 use web_sys::MouseEvent;
-use yew::{function_component, html, Callback, Properties};
+use yew::{function_component, html, virtual_dom::AttrValue, Callback, Children, Properties};
 
 #[derive(Properties, PartialEq)]
 pub struct Props {
     pub disabled: bool,
     pub onclick: Callback<MouseEvent>,
+    // Additional elements to insert after the button, in the same div
+    #[prop_or_default]
+    pub children: Children,
+    #[prop_or(AttrValue::from("Submit"))]
+    pub text: AttrValue,
 }
 
 #[function_component(Submit)]
 pub fn submit(props: &Props) -> Html {
     html! {
-        <div class="form-group row justify-content-center">
-            <button
-                class="btn btn-primary col-auto col-form-label"
-                type="submit"
-                disabled={props.disabled}
-                onclick={&props.onclick}>
-                <i class="bi-save me-2"></i>
-                {"Submit"}
-            </button>
-        </div>
+      <div class="form-group row justify-content-center">
+        <button
+          class="btn btn-primary col-auto col-form-label"
+          type="submit"
+          disabled={props.disabled}
+          onclick={&props.onclick}>
+          <i class="bi-save me-2"></i>
+          {props.text.clone()}
+        </button>
+        {for props.children.iter()}
+      </div>
     }
 }
diff --git a/app/src/components/login.rs b/app/src/components/login.rs
index 8d35be8c..b78ffc58 100644
--- a/app/src/components/login.rs
+++ b/app/src/components/login.rs
@@ -1,5 +1,8 @@
 use crate::{
-    components::router::{AppRoute, Link},
+    components::{
+        form::submit::Submit,
+        router::{AppRoute, Link},
+    },
     infra::{
         api::HostService,
         common_component::{CommonComponent, CommonComponentParts},
@@ -155,68 +158,62 @@ impl Component for LoginForm {
             }
         } else {
             html! {
-              <form
-                class="form center-block col-sm-4 col-offset-4">
-                  <div class="input-group">
-                    <div class="input-group-prepend">
-                      <span class="input-group-text">
-                        <i class="bi-person-fill"/>
-                      </span>
-                    </div>
-                    <Field
-                      class="form-control"
-                      class_invalid="is-invalid has-error"
-                      class_valid="has-success"
-                      form={&self.form}
-                      field_name="username"
-                      placeholder="Username"
-                      autocomplete="username"
-                      oninput={link.callback(|_| Msg::Update)} />
-                  </div>
-                  <div class="input-group">
-                    <div class="input-group-prepend">
-                      <span class="input-group-text">
-                        <i class="bi-lock-fill"/>
-                      </span>
-                    </div>
-                    <Field
-                      class="form-control"
-                      class_invalid="is-invalid has-error"
-                      class_valid="has-success"
-                      form={&self.form}
-                      field_name="password"
-                      input_type="password"
-                      placeholder="Password"
-                      autocomplete="current-password" />
+              <form class="form center-block col-sm-4 col-offset-4">
+                <div class="input-group">
+                  <div class="input-group-prepend">
+                    <span class="input-group-text">
+                      <i class="bi-person-fill"/>
+                    </span>
                   </div>
-                  <div class="form-group mt-3">
-                    <button
-                      type="submit"
-                      class="btn btn-primary"
-                      disabled={self.common.is_task_running()}
-                      onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})}>
-                      <i class="bi-box-arrow-in-right me-2"/>
-                      {"Login"}
-                    </button>
-                    { if password_reset_enabled {
-                      html! {
-                        <Link
-                          classes="btn-link btn"
-                          disabled={self.common.is_task_running()}
-                          to={AppRoute::StartResetPassword}>
-                          {"Forgot your password?"}
-                        </Link>
-                      }
-                    } else {
-                      html!{}
-                    }}
-                  </div>
-                  <div class="form-group">
-                  { if let Some(e) = &self.common.error {
-                      html! { e.to_string() }
-                    } else { html! {} }
-                  }
+                  <Field
+                    class="form-control"
+                    class_invalid="is-invalid has-error"
+                    class_valid="has-success"
+                    form={&self.form}
+                    field_name="username"
+                    placeholder="Username"
+                    autocomplete="username"
+                    oninput={link.callback(|_| Msg::Update)} />
+                </div>
+                <div class="input-group">
+                  <div class="input-group-prepend">
+                    <span class="input-group-text">
+                      <i class="bi-lock-fill"/>
+                    </span>
                   </div>
+                  <Field
+                    class="form-control"
+                    class_invalid="is-invalid has-error"
+                    class_valid="has-success"
+                    form={&self.form}
+                    field_name="password"
+                    input_type="password"
+                    placeholder="Password"
+                    autocomplete="current-password" />
+                </div>
+                <Submit
+                  text="Login"
+                  disabled={self.common.is_task_running()}
+                  onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})}>
+                  { if password_reset_enabled {
+                    html! {
+                      <Link
+                        classes="btn-link btn"
+                        disabled={self.common.is_task_running()}
+                        to={AppRoute::StartResetPassword}>
+                        {"Forgot your password?"}
+                      </Link>
+                    }
+                  } else {
+                    html!{}
+                  }}
+                </Submit>
+                <div class="form-group">
+                { if let Some(e) = &self.common.error {
+                    html! { e.to_string() }
+                  } else { html! {} }
+                }
+                </div>
               </form>
             }
         }
diff --git a/app/src/components/reset_password_step2.rs b/app/src/components/reset_password_step2.rs
index 7dfece62..a63cdb97 100644
--- a/app/src/components/reset_password_step2.rs
+++ b/app/src/components/reset_password_step2.rs
@@ -1,5 +1,8 @@
 use crate::{
-    components::router::{AppRoute, Link},
+    components::{
+        form::{field::Field, submit::Submit},
+        router::{AppRoute, Link},
+    },
     infra::{
         api::HostService,
         common_component::{CommonComponent, CommonComponentParts},
@@ -164,61 +167,29 @@ impl Component for ResetPasswordStep2Form {
             }
             _ => (),
         };
-        type Field = yew_form::Field<FormModel>;
         html! {
           <>
             <h2>{"Reset your password"}</h2>
-            <form
-              class="form">
-              <div class="form-group row">
-                <label for="new_password"
-                  class="form-label col-sm-2 col-form-label">
-                  {"New password*:"}
-                </label>
-                <div class="col-sm-10">
-                  <Field
-                    form={&self.form}
-                    field_name="password"
-                    class="form-control"
-                    class_invalid="is-invalid has-error"
-                    class_valid="has-success"
-                    autocomplete="new-password"
-                    input_type="password"
-                    oninput={link.callback(|_| Msg::FormUpdate)} />
-                  <div class="invalid-feedback">
-                    {&self.form.field_message("password")}
-                  </div>
-                </div>
-              </div>
-              <div class="form-group row">
-                <label for="confirm_password"
-                  class="form-label col-sm-2 col-form-label">
-                  {"Confirm password*:"}
-                </label>
-                <div class="col-sm-10">
-                  <Field
-                    form={&self.form}
-                    field_name="confirm_password"
-                    class="form-control"
-                    class_invalid="is-invalid has-error"
-                    class_valid="has-success"
-                    autocomplete="new-password"
-                    input_type="password"
-                    oninput={link.callback(|_| Msg::FormUpdate)} />
-                  <div class="invalid-feedback">
-                    {&self.form.field_message("confirm_password")}
-                  </div>
-                </div>
-              </div>
-              <div class="form-group row mt-2">
-                <button
-                  class="btn btn-primary col-sm-1 col-form-label"
-                  type="submit"
-                  disabled={self.common.is_task_running()}
-                  onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})}>
-                  {"Submit"}
-                </button>
-              </div>
+            <form class="form">
+              <Field<FormModel>
+                label="New password"
+                required=true
+                form={&self.form}
+                field_name="password"
+                autocomplete="new-password"
+                input_type="password"
+                oninput={link.callback(|_| Msg::FormUpdate)} />
+              <Field<FormModel>
+                label="Confirm password"
+                required=true
+                form={&self.form}
+                field_name="confirm_password"
+                autocomplete="new-password"
+                input_type="password"
+                oninput={link.callback(|_| Msg::FormUpdate)} />
+              <Submit
+                disabled={self.common.is_task_running()}
+                onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})} />
             </form>
             { if let Some(e) = &self.common.error {
                 html! {
diff --git a/app/src/components/user_details_form.rs b/app/src/components/user_details_form.rs
index 56100c63..663e78b8 100644
--- a/app/src/components/user_details_form.rs
+++ b/app/src/components/user_details_form.rs
@@ -1,7 +1,10 @@
 use std::str::FromStr;
 
 use crate::{
-    components::user_details::User,
+    components::{
+        form::{field::Field, static_value::StaticValue, submit::Submit},
+        user_details::User,
+    },
     infra::common_component::{CommonComponent, CommonComponentParts},
 };
 use anyhow::{bail, Error, Result};
@@ -183,7 +186,6 @@ impl Component for UserDetailsForm {
     }
 
     fn view(&self, ctx: &Context<Self>) -> Html {
-        type Field = yew_form::Field<UserModel>;
         let link = &ctx.link();
 
         let avatar_string = match &self.avatar {
@@ -196,107 +198,40 @@ impl Component for UserDetailsForm {
         html! {
           <div class="py-3">
             <form class="form">
-              <div class="form-group row mb-3">
-                <label for="userId"
-                  class="form-label col-4 col-form-label">
-                  {"User ID: "}
-                </label>
-                <div class="col-8">
-                  <span id="userId" class="form-control-static"><i>{&self.user.id}</i></span>
-                </div>
-              </div>
-              <div class="form-group row mb-3">
-                <label for="creationDate"
-                  class="form-label col-4 col-form-label">
-                  {"Creation date: "}
-                </label>
-                <div class="col-8">
-                  <span id="creationDate" class="form-control-static">{&self.user.creation_date.naive_local().date()}</span>
-                </div>
-              </div>
-              <div class="form-group row mb-3">
-                <label for="uuid"
-                  class="form-label col-4 col-form-label">
-                  {"UUID: "}
-                </label>
-                <div class="col-8">
-                  <span id="creationDate" class="form-control-static">{&self.user.uuid}</span>
-                </div>
-              </div>
-              <div class="form-group row mb-3">
-                <label for="email"
-                  class="form-label col-4 col-form-label">
-                  {"Email"}
-                  <span class="text-danger">{"*"}</span>
-                  {":"}
-                </label>
-                <div class="col-8">
-                  <Field
-                    class="form-control"
-                    class_invalid="is-invalid has-error"
-                    class_valid="has-success"
-                    form={&self.form}
-                    field_name="email"
-                    autocomplete="email"
-                    oninput={link.callback(|_| Msg::Update)} />
-                  <div class="invalid-feedback">
-                    {&self.form.field_message("email")}
-                  </div>
-                </div>
-              </div>
-              <div class="form-group row mb-3">
-                <label for="display_name"
-                  class="form-label col-4 col-form-label">
-                  {"Display Name: "}
-                </label>
-                <div class="col-8">
-                  <Field
-                    class="form-control"
-                    class_invalid="is-invalid has-error"
-                    class_valid="has-success"
-                    form={&self.form}
-                    field_name="display_name"
-                    autocomplete="name"
-                    oninput={link.callback(|_| Msg::Update)} />
-                  <div class="invalid-feedback">
-                    {&self.form.field_message("display_name")}
-                  </div>
-                </div>
-              </div>
-              <div class="form-group row mb-3">
-                <label for="first_name"
-                  class="form-label col-4 col-form-label">
-                  {"First Name: "}
-                </label>
-                <div class="col-8">
-                  <Field
-                    class="form-control"
-                    form={&self.form}
-                    field_name="first_name"
-                    autocomplete="given-name"
-                    oninput={link.callback(|_| Msg::Update)} />
-                  <div class="invalid-feedback">
-                    {&self.form.field_message("first_name")}
-                  </div>
-                </div>
-              </div>
-              <div class="form-group row mb-3">
-                <label for="last_name"
-                  class="form-label col-4 col-form-label">
-                  {"Last Name: "}
-                </label>
-                <div class="col-8">
-                  <Field
-                    class="form-control"
-                    form={&self.form}
-                    field_name="last_name"
-                    autocomplete="family-name"
-                    oninput={link.callback(|_| Msg::Update)} />
-                  <div class="invalid-feedback">
-                    {&self.form.field_message("last_name")}
-                  </div>
-                </div>
-              </div>
+              <StaticValue label="User ID" id="userId">
+                <i>{&self.user.id}</i>
+              </StaticValue>
+              <StaticValue label="Creation date" id="creationDate">
+                {&self.user.creation_date.naive_local().date()}
+              </StaticValue>
+              <StaticValue label="UUID" id="uuid">
+                {&self.user.uuid}
+              </StaticValue>
+              <Field<UserModel>
+                form={&self.form}
+                required=true
+                label="Email"
+                field_name="email"
+                input_type="email"
+                oninput={link.callback(|_| Msg::Update)} />
+              <Field<UserModel>
+                form={&self.form}
+                label="Display name"
+                field_name="display_name"
+                autocomplete="name"
+                oninput={link.callback(|_| Msg::Update)} />
+              <Field<UserModel>
+                form={&self.form}
+                label="First name"
+                field_name="first_name"
+                autocomplete="given-name"
+                oninput={link.callback(|_| Msg::Update)} />
+              <Field<UserModel>
+                form={&self.form}
+                label="Last name"
+                field_name="last_name"
+                autocomplete="family-name"
+                oninput={link.callback(|_| Msg::Update)} />
               <div class="form-group row align-items-center mb-3">
                 <label for="avatar"
                   class="form-label col-4 col-form-label">
@@ -340,16 +275,10 @@ impl Component for UserDetailsForm {
                   </div>
                 </div>
               </div>
-              <div class="form-group row justify-content-center mt-3">
-                <button
-                  type="submit"
-                  class="btn btn-primary col-auto col-form-label"
-                  disabled={self.common.is_task_running()}
-                  onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitClicked})}>
-                  <i class="bi-save me-2"></i>
-                  {"Save changes"}
-                </button>
-              </div>
+              <Submit
+                text="Save changes"
+                disabled={self.common.is_task_running()}
+                onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitClicked})} />
             </form>
             {
               if let Some(e) = &self.common.error {


Test patch: