Search code examples
aurelia

How can I compose a VM into a view within an Aurelia validation renderer


I'm trying to use the aurelia-validation plugin to perform validation on a form. I'm creating a custom validation renderer that will change the color of the input box as well as place an icon next to the box. When the icon is clicked or hovered, a popup message appears that will display the actual error message.

enter image description here

Currently, I'm rendering all of this in code manually in the renderer, but it seems like it would be nice to have the html for all of this defined in an html file along with the associated js file to handle the click and hover on the icon. IOW, encapsulate all the error stuff (icon with popup) in a View/ViewModel and then in the render() of my validation renderer, somehow just compose a new instance of this just after the element in question.

Is this possible to do? I've seen how to use <compose></compose> element but I really am trying to avoid having to add that to all of my forms' input boxes.

This is what I currently have in my renderer:

import {ValidationError, RenderInstruction} from 'aurelia-validation'

export class IconValidationRenderer {
    render(instruction){
        //Unrender old errors
        for(let {result, elements} of instruction.unrender){
            for(let element of elements){
                this.remove(element, result);
            }
        }

        //Render new errors
        for(let {result, elements} of instruction.render){
            for(let element of elements){
                this.add(element, result)
            }
        }
    }

    add(element, result){
        if(result.valid) 
            return

        //See if error element already exists
        if(element.className.indexOf("has-error") < 0){
            let errorIcon = document.createElement("i")
            errorIcon.className = "fa fa-exclamation-circle"
            errorIcon.style.color = "darkred"
            errorIcon.style.paddingLeft = "5px"
            errorIcon.id = `error-icon-${result.id}`
            errorIcon.click = ""
            element.parentNode.appendChild(errorIcon)

            element.classList.add("has-error")
            element.parentNode.style.alignItems = "center"

            let errorpop = document.createElement("div")
            let errorarrow = document.createElement("div")
            let errorbody = document.createElement("div")
            errorpop.id = `error-pop-${result.id}`
            errorpop.className = "flex-row errorpop"
            errorarrow.className = "poparrow"
            errorbody.className = "flex-col popmessages"
            errorbody.innerText = result.message
            console.log("Computing position")

            let elemRec = errorIcon.getBoundingClientRect()
            let elemH = errorIcon.clientHeight
            errorpop.style.top = elemRec.top - 10 + "px"
            errorpop.style.left = elemRec.right + "px"

            errorpop.appendChild(errorarrow)
            errorpop.appendChild(errorbody)
            element.parentNode.appendChild(errorpop)
        }
    }

    remove(element, result){
        if(result.valid)
            return

        element.classList.remove("has-error")
        let errorIcon = element.parentNode
            .querySelector(`#error-icon-${result.id}`)
        if(errorIcon)
            element.parentNode.removeChild(errorIcon)

        //Need to remove validation popup element
    }
}

Thanks for any help you can offer.

P.S. At this point, I am not implementing a click or hover like I mentioned -- that is something that I would like to do but I'm not even sure how at this point. Would be more straight forward if I can compose a VM.

EDIT

I was pointed to this article by someone on the Aurelia Gitter channel. I've tried implementing the TemplatingEngine but clearly I'm not going about it the right way. Here's what I have.

add-person-dialog.js //VM that has form with validation

import {TemplatingEngine,NewInstance} from 'aurelia-framework'
import {ValidationController} from 'aurelia-validation'
import {IconValidationRenderer} from './resources/validation/icon-validation-renderer'

export class AddPersonDialog {
     static inject = [NewInstance.of(ValidationController),TemplatingEngine]

     constructor(vc, te){
         this.vc = vc
         this.vc.addRenderer(new IconValidationRenderer(te))
     }

icon-validation-renderer.js

//Plus all the other bits that I posted in the code above
constructor(te){
   this.te = te
}

add(element, result){
    if(result.valid) return

    if(element.className.indexOf("has-error") < 0 {
        //replaced there error icon code above with this (as well as a few different variations
        let test = document.createElement("field-error-info")
        element.parentNode.appendChild(test)
        this.te.enhance({element: test})
    }
 }

field-error-info.html

<template>
    <require from="./field-error-info.css" ></require>

    <i class="fa fa-exclamation-circle" click.delegate="displayMessage = !displayMessage" mouseenter.delegate="displayMessage = true" mouseleave.delegate="displayMessage = false"></i>
    <div show.bind="displayMessage" class="flex-row errorpop" style="left:300px">
        <div class="poparrow"></div>
        <div class="flexcol popmessages">Message 1</div>
    </div>
</template>

Ultimately, <field-error-info></field-error-info> gets added to the DOM but doesn't actually get rendered. (Incidentally, I also tried adding <require from='./elements/field-error-info'></require> in the add-person-dialog.html.


Solution

  • You could create a form control custom element that encapsulates the error icon and tooltip logic. The element could expose two content projection slots to enable passing in a label and input/select/etc:

    <template>
      <div validation-errors.bind="errors"
           class="form-group ${errors.length ? 'has-error' : ''}">
    
        <!-- label slot -->
        <slot name="label"></slot>
    
        <!-- input slot -->
        <slot name="input"></slot>
    
        <!-- icon/tooltip stuff -->
        <span class="control-label glyphicon glyphicon-exclamation-sign tooltips"
              show.bind="errors.length">
          <span>
            <span repeat.for="errorInfo of errors">${errorInfo.error.message}</span>
          </span>
        </span>
      </div>
    </template>
    

    Here's how it would be used:

    <template>
      <require from="./form-control.html"></require>
    
      <form novalidate autofill="off">
    
        <form-control>
          <label slot="label" for="firstName" class="control-label">First Name:</label>
          <input slot="input" type="text" class="form-control" 
                 value.bind="firstName & validateOnChange">
        </form-control>
    
        <form-control>
          <label slot="label" for="lastName" class="control-label">Last Name:</label>
          <input slot="input" type="text" class="form-control" 
                 value.bind="lastName & validateOnChange">
        </form-control>
    
      </form>
    </template>
    

    Live example: https://gist.run/?id=874b100da054559929d5761bdeeb651c

    please excuse the crappy tooltip css