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
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 */
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:
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:
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">
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
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.
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.
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