Validation style final tweaks

To add the missing styles to make it as functional as possible, need to keep reminding ourselves of a golden rule: Do not assume. This means we can add relative paddings and margins, borders and colors, using CSS variables, but we cannot dictate how the checkbox should look like. That’s a project style, not an element style. (There are schools like material design that stuff their noses into every element, making it impossible to use single components individually.) Checkbox Starting with the checkbox, all we need to do is swap locations of checkbox and label. Then just let the outer project design its own checkbox instead. There are two solutions, a smart one, and a too smart one. The too smart one looks like this: .cr-field { /* target previous silbing */ .cr-label:has(~ [type="checkbox"]) { /* important to remove transform in all cases */ transform: none!important; inset-block-start: 0; inset-inline-start: 0; padding-inline-start: 1.8rem; position: relative; display: inline-block; background: none; cursor: pointer; } .cr-input[type="checkbox"] { position: absolute; inset-inline-start: 0; } } The simpler way is to introduce a new type property for the cr-field directly, assigned explicitly. Implicit assignment is also "too smart." // input.partial template: ` // ... ` @Input() type!: string; get typeCss(): string { return this.type ? `${this.cssPrefix}-${this.type}` : ''; } Then we write a not too-smart CSS: .cr-field.cr-checkbox { .cr-label { /* same as above */ } .cr-input { /* same as above */ } } The selector is simpler and we have a bit more room to style other things, like the required asterisk, the help text, and the feedback text. See, sometimes it pays off to be dumber. The CSS for this solution: .cr-field.cr-checkbox { .cr-label { /* important to remove transform in all cases */ transform: none!important; inset-block-start: 0; inset-inline-start: 0; padding-inline-start: 1.8rem; position: relative; display: inline-block; background: none; cursor: pointer; } .cr-input { position: absolute; inset-inline-start: 0; } .cr-feedback { margin-block-start: 0; float: none; } .cr-required { position: static; } } Testing the edges One of the scenarios we went through is where the required asterisk was out of view, we already designed it to be positioned on the extreme right. With what we have so far, can we make it look good without touching the library component and shared css? Here is the example of Expiry date, the solution to place the asterisk within context is simply add enough style to the container. /* fix the container width to be c-5 and display block */ /* also fix the width of inner fields to c-6 */ The result is like this: Three things I’ve done: Changed the container width to be at the percentage width I desired, and changed its display to block (Angular components display by default as contents ) I changed the inner component width to be 50% each (makes more sense) Then changed the message to: Add a date in the future. This covers both rules: expired date and required value. So our component held. No changes needed. That is a victory. You may need to test extra scenarios and update the input css to be as simple as possible, so that it won’t break in the future. Testing another edge case: Nice looking checkbox Here is another edge case. Every project has its own style for checkboxes. Given the CSS we have developed thus far, let's see if we can push our single checkbox from the outside world without breaking it. Let's use MDN example. /* adding the same css with a proper random selector to our project */ .gr-something .cr-field.cr-checkbox { .cr-input { /* remove default appearance **/ appearance: none; width: 44px; height: 24px; border-radius: 12px; transition: all 0.4s; } .cr-input::before { width: 16px; height: 16px; border-radius: 9px; background-color: var(--sh-black, #000); content: ''; position: absolute; inset-block-start: 3px; inset-inline-start: 4px; transition: all 0.4s; } .cr-input:checked { background-color: var(--sh-yellow, #ffaa00); transition: all 0.4s; } .cr-input:checked::before { inset-inline-start: 22px; transition: all 0.4s; } .cr-label { /* adjust padding of label */ padding-inline-start: 4.2rem; } } The above is the MDN example with few adjustments. We just had to pay attention to the selector so that we don't resolve to !important issues. Applying it is as easy as applying class to selector: This looks like the following: Another victory! See, if we were too smart about our selectors, this would have turned into spaghetti. Hidden fields Hidden inputs simplify validation, that would ot

Jan 17, 2025 - 11:09
Validation style final tweaks

To add the missing styles to make it as functional as possible, need to keep reminding ourselves of a golden rule:

Do not assume.

This means we can add relative paddings and margins, borders and colors, using CSS variables, but we cannot dictate how the checkbox should look like. That’s a project style, not an element style. (There are schools like material design that stuff their noses into every element, making it impossible to use single components individually.)

Checkbox

Starting with the checkbox, all we need to do is swap locations of checkbox and label. Then just let the outer project design its own checkbox instead. There are two solutions, a smart one, and a too smart one. The too smart one looks like this:

.cr-field {
   /* target previous silbing */
  .cr-label:has(~ [type="checkbox"]) {
    /* important to remove transform in all cases */
    transform: none!important;
    inset-block-start: 0;
    inset-inline-start: 0;
    padding-inline-start: 1.8rem;
    position: relative;
    display: inline-block;
    background: none;
    cursor: pointer;
  }

  .cr-input[type="checkbox"] {
    position: absolute;
    inset-inline-start: 0;
  }
}

The simpler way is to introduce a new type property for the cr-field directly, assigned explicitly. Implicit assignment is also "too smart."

// input.partial
template: `
    
// ... ` @Input() type!: string; get typeCss(): string { return this.type ? `${this.cssPrefix}-${this.type}` : ''; }

Then we write a not too-smart CSS:

.cr-field.cr-checkbox {
  .cr-label {
   /* same as above */
  }
  .cr-input {
   /* same as above */
  }
}

The selector is simpler and we have a bit more room to style other things, like the required asterisk, the help text, and the feedback text. See, sometimes it pays off to be dumber.

Checkbox validation

The CSS for this solution:

.cr-field.cr-checkbox {
  .cr-label {
    /* important to remove transform in all cases */
    transform: none!important;
    inset-block-start: 0;
    inset-inline-start: 0;
    padding-inline-start: 1.8rem;
    position: relative;
    display: inline-block;
    background: none;
    cursor: pointer;
  }
  .cr-input {
    position: absolute;
    inset-inline-start: 0;
  }
  .cr-feedback {
    margin-block-start: 0;
    float: none;
  }
  .cr-required {
    position: static;
  }
}

Testing the edges

One of the scenarios we went through is where the required asterisk was out of view, we already designed it to be positioned on the extreme right. With what we have so far, can we make it look good without touching the library component and shared css?

Here is the example of Expiry date, the solution to place the asterisk within context is simply add enough style to the container.

/* fix the container width to be c-5 and display block */
 placeholder="Expiration" error="Add a date in the future" class="c-5 dblock">
   type="hidden" crinput id="mmyy" [required]="true" pattern="[0-9]{4}" formControlName="mmyy" />
  /* also fix the width of inner fields to c-6 */
   (onValue)="expirationValue($event)">


The result is like this:

Component validation

Three things I’ve done:

  • Changed the container width to be at the percentage width I desired, and changed its display to block (Angular components display by default as contents )
  • I changed the inner component width to be 50% each (makes more sense)
  • Then changed the message to: Add a date in the future. This covers both rules: expired date and required value.

So our component held. No changes needed. That is a victory.

You may need to test extra scenarios and update the input css to be as simple as possible, so that it won’t break in the future.

Testing another edge case: Nice looking checkbox

Here is another edge case. Every project has its own style for checkboxes. Given the CSS we have developed thus far, let's see if we can push our single checkbox from the outside world without breaking it. Let's use MDN example.

/* adding the same css with a proper random selector to our project */
.gr-something .cr-field.cr-checkbox {

  .cr-input {
    /* remove default appearance **/
    appearance: none;
    width: 44px;
    height: 24px;
    border-radius: 12px;
    transition: all 0.4s;
  }
  .cr-input::before {
    width: 16px;
    height: 16px;
    border-radius: 9px;
    background-color: var(--sh-black, #000);
    content: '';
    position: absolute;
    inset-block-start: 3px;
    inset-inline-start: 4px;
    transition: all 0.4s;
  }
  .cr-input:checked {
    background-color: var(--sh-yellow, #ffaa00);
    transition: all 0.4s;
  }
  .cr-input:checked::before {
    inset-inline-start: 22px;
    transition: all 0.4s;
  }
  .cr-label {
    /* adjust padding of label */
    padding-inline-start: 4.2rem;
  }
}

The above is the MDN example with few adjustments. We just had to pay attention to the selector so that we don't resolve to !important issues. Applying it is as easy as applying class to selector:

  type="checkbox" class="gr-something" ...>
     type="checkbox" crinput  ...>

This looks like the following:

Flip switch validation

Another victory! See, if we were too smart about our selectors, this would have turned into spaghetti.

Hidden fields

Hidden inputs simplify validation, that would otherwise be challenging. If the validation is within context of the cr-field it’s straight forward, like our previous example of the Expiration date.

 placeholder="Expiration" error="This is expired">
   type="hidden" crinput id="mmyy" pattern="[0-9]{4}" formControlName="mmyy" />
  // some component with MM and YY fields
   (onValue)="expirationValue($event)">

If the hidden input is not within context of a field, and it carries out a cross-form update and validation, then the cr-field has nothing but the feedback. The solution is to introduce the type hidden.

Here is a quick example, let’s say if the operating system is MAC, the minimum version is 5.

// example form components
 class="spaced">
   placeholder="Operating system">
     crinput id="os" formControlName="os" [required]="true" (change)="updatePlug()">
       value="">Select
       value="1">Windows
       value="2">Mac
       value="3">Linux
       value="4">Android
    
  
placeholder="Version"> crinput type="number" id="version" formControlName="version" (change)="updatePlug()" /> helptext>OS version

Updating the fields updates a hidden field plugs with either a value or null

template: `  
// ...

  
`

// in code
updatePlug() {
  // on change of form input, update hidden field
  const os = this.fg.get('os').value;
  const version = this.fg.get('version').value;
  if (os === '2' && version < 5) {
    this.fg.get('plugs').setValue(null);
  } else {
    this.fg.get('plugs').setValue('plug'+ os + version);
  }
}

So the only thing I want to take care of is hiding the required asterisk, and making the feedback more of a none-floating element. Any other styling, need to happen outside the component.

// let's pass type:hidden
 type="hidden" error="Not allowed to have Mac version less than 5">
   type="hidden" crinput id="plugs" formControlName="plugs" [required]="true" >

And we cater for the cr-hidden style:

// input.css
.cr-field.cr-hidden {
  .cr-label {
    display: none;
  }
  .cr-input[required] ~ .cr-required {
    display: none;
  }
  .cr-feedback {
    float: none;
    margin-block-start: 0;
    margin-inline-start: 0;
  }
}

This will look like this

Hidden validation

Of course, my validation is a bit dumbed down for demonstration purposes. The hidden field can have any validation (a pattern for example), you can use setErrors instead of setValue, and you can customize the error message accordingly. That would not affect the CSS though.

Auto-filled fields

The last problem to fix is auto-filled fields, like username and password, where you don’t want the placeholder label to appear on top of the field. Ever. To fix that we introduce a new type static to prevent the floating animation.

Autofill login

Although this isn't stubborn but it would be nice to have another style for none-floating labels in general.

 placeholder="Username" type="static">
     crinput type="text" autocomplete="username" id="username"
     formControlName="username" [required]="true" />

 placeholder="Password" type="static">
     crinput type="password" autocomplete="current-password" id="pwd" 
    formControlName="pwd" [required]="true" />

The CSS then defines the new type:

/* input.css */
.cr-field.cr-static {
  /* force floating label even if empty */
  .cr-label:has(~ .cr-input:placeholder-shown) {
    transform: translateY(-100%) scale(0.8);
  }
}

And this is as good as it gets. Notice that if emptied, that label will not float inside the field. Meh, cheap price.

Static fields

That's it. This is a wrap.

Bonus: standalone imports

Another thing we can do to make this easier to use, is place them in an exported const to import together.

export const InputComponent = [
  InputDirective,
  CrInputPartial
];

Then we can import then together

// in our component
// ...
imports: [...InputComponent],

Conclusion

To recap, the initial target was the following:

  • Use native HTML input elements.
    We did, with only the introduction a formControlName and a directive

  • Validation rules should be kept to minimum
    We only manipulated the error message, and contained basic patterns and common functions

  • Keep the Angular form loose (do not reinvent the wheel)
    The input is content-projected, it belongs to the original form

  • Use attributes instead of Form builder (unobtrusive)
    A directive that reads different attributes was accomplished

  • Keep form submission loose to allow as much flexibility
    Since the form is not part of our directive, nothing is imposed on it

  • Minimum styling allowing full replacement.
    We tried. I think we managed.

That's it. Let Gaza Live.

Resources

Validation style final tweaks - Sekrab Garage

Taming Angular forms. To add the missing styles to make it as functional as possible, need to keep reminding ourselves of a golden rule:Do not assume.This means we can add relative paddings and margins, borders and colors,.... Posted in Angular, Design

favicon garage.sekrab.com