4

I'm trying to add an aria-describedby attribute to ui:inputText to programmatically associate some inline help text with the input field for assistive technology.

enter image description here

The resulting markup should look something like this:

<input type="text" aria-describedby="expnameHelp" />
<div id="expnameHelp">Some help text</div>

The relevant component markup currently looks like this:

<div class="slds-form-element slds-is-required">
  <div class="slds-form-element__control">
    <ui:inputText aura:id="expname" label="Expense Name"
      class="slds-input"
      labelClass="slds-form-element__label"
      value="{!v.newExpense.Name}"
      required="true" />
 </div>
 <div aura:id="expnameHelp" class="help-block">
   <p>Please use the format: Date - last name - purpose</p>
 </div>

However, there is no way I can see to set aria-describedby on the ui:inputText component, and according to this thread, there is no way to set a pass-through attribute. I would expect something like ariaDescribedBy="expnameHelp" as an attribute on the ui:inputText tag, but this doesn't seem possible.

I thought maybe I could set the attribute dynamically using JavaScript, so I added the following to the controller:

doInit : function(component, event, helper) {
  // Programmatically associate help text with form element
  helper.updateHelpText(component);
}

And the following to the helper:

updateHelpText: function(component) {
  // Set the aria-describedby attribute on the expense name component
  var expnameField = component.find('expname');
  var expnameHelpDiv = component.find('expnameHelp');
  expnameHelpDiv.set('v.id', expnameHelpDiv.getGlobalId());
  expnameField.set('v.aria-describedby', expnameHelpDiv.getGlobalId());
}

I can now access the values of those attributes using JavaScript, but when I inspect the DOM the changes aren't visible.

What am I missing? Also, this seems like a convoluted approach -- am I missing a simpler solution?

UPDATE #1: As per Peter's suggestion, I moved the updateHelpText() helper function call to afterrender (and also tried render, and rerrender), but the changes still don't show up in the DOM.

afterrender: function(cmp, helper) {
  var ret = this.superAfterRender();
  helper.updateHelpText(cmp);
}

UPDATE #2: Thanks to Peter's additional comments, I have the following now working. However, I'm wondering if there is a better way to reference the actual input HTML element in the component?

Renderer JS:

// Case is important!
afterRender: function(cmp, helper) {
  this.superAfterRender();
  helper.updateHelpText(cmp);
}

Helper JS:

updateHelpText: function(component) {
  // Set the aria-describedby attribute on the expense name component -
  var expnameElm = component.find('expname').getElement();

  // Turns out component.find('expname').getElement() returns the
  // div that contains the input field, not the actual input field
  // so getting that via getElementsByTagName - better way?
  expnameElm.getElementsByTagName('input')[0].setAttribute('aria-describedby', 'expnameHelp');
}
  • 1
    If you're changing the DOM, you should be doing it in the renderer, in an afterRender function. The DOM isn't necessarily accessible during init. That doesn't answer the actual aria problems. I understood there were a lot of aria attributes that we were adding automatically to LCF, but I'll be honest, had not investigated it very closely. – pchittum Jan 17 '16 at 20:46
  • The aria attributes do get set automagically for error messages, but I don't see how to do them explicitly. I'll explore the rendering, thanks! – shannonsans Jan 17 '16 at 20:50
  • 2
    Oh...I see what you're doing. You need to do a direct-to HTML attribute, not an Aura attribute. Like here: w3schools.com/jsref/met_document_createattribute.asp – pchittum Jan 17 '16 at 21:16
  • added some more to my answer to show how to get the HTML element from the lightning component. – pchittum Jan 17 '16 at 21:31
  • 1
    As a heads up this direct reaching inside of a component's internals that you do not own is not supported and with the upcoming security model called lockerService this code will no longer even be possible. – Doug Chasman Jan 19 '16 at 20:26
5

As a heads up this direct reaching inside of a component's internals that you do not own is not supported and with the upcoming security model enhancement called LockerService this code will no longer even be possible.

This line will fail:

var expnameElm = component.find('expname').getElement();

and even if you tried to go directly to the DOM you are going to be blocked by the security infrastructure that applies fine grained object capability locking to the DOM (and all Lightning APIs too).

because only the ui:inputText should depend on its internal private parts otherwise there is no way for the component's author to maintain API compatibility release over release. This is the tradeoff for having push upgrades in an enterprise grade environment - everything needs to be formal API contracts or there is no way that Salesforce can guarantee backward compatibility.

I have brought this to the attention of the folks that own the ui:visible interface that defines v.ariaDescribedBy to see if they can add the access="GLOBAL" to this to make it visible so your original approach of just setting ariaDescribedBy= would work.

  • 1
    So basically you're saying this can't be done in a supported way in the current release (Winter '16 and probably even Spring '16). – shannonsans Jan 19 '16 at 23:47
5

If you look at the original Aura open source project there are attributes in the ui:input component for ariaDescibedBy. But neither ui:input or the aria attribute seem to have made the jump to global scope to be surfaced in the Lightning Component framework on the platform.

Your best bet to get to the DOM is to make changes using a renderer.

To familiarize yourself with the component rendering lifecycle, I'd suggest taking a look at the documentation. But the short version goes like this:

  • Init should be used primarily for getting data into place that your component will need to work. But happens before the DOM is available.
  • The renderer is something every component has, and is what gets used to turn the component into some kind of HTML fragment to be inserted into the DOM.
  • There are events you can override like render, afterRender, and reRender.
  • Anything you want to do that touches the DOM should be placed here and not in an event handler (in other words a controller), as this can create race conditions (done it...ain't pretty).

Typically you want to do the following:

({
  //the name of the function has to be this
  afterRender: function(component, helper) {
    //make sure you do the default render behavior...
    //this ensure you get the component you actually want, like an 
    //input, or button...without it, you get nothing
    this.superAfterRender();

    //call a helper function that actually renders your DOM object
    helper.addAria(component);
  }
})  

In my case, I was drawing on an HTML canvas. Putting the doChart function here made sure the canvas html element had actually been created before I went to get canvas context. This could be the problem your having.

In your case, you'd need to get the actual HTML element, and then set the HTML attribute. Something like this:

addAria : function(component){

  //get the actual HTML DOM element for this component
  var thisComponent = component.getElement(); 

  //create an attribute;
  var ariaAttr = document.createAttribute("aria-desribedby"); 

  //set the value of my attribute
  ariaAttr.value = "expnameHelp"; 

  //set the attribute into the DOM element
  thisComponent.setAttributeNode(ariaAttr); 
}

Disclaimer: untested code above

It is important to remember the difference between a lightning component attribute, and an HTML DOM attribute.

The lightning component attribute sometime will have to do with how the component is rendered...but is not necessarily what the component will look like once rendered into the DOM of the page.

HTML attributes are actual DOM data, and will be what the browser interprets to create the HTML page you eventually display.

Anytime you are doing component.get('v.someAttribute') this is the lightning component attribute.

Anytime you are doing component.getElement() you will have a handle to the actual HTML elements of your page...but this is only meant to work from the context of the renderer.

  • shoot...just saw your update... – pchittum Jan 17 '16 at 21:13
  • 1
    Most important line here: "//the name of the function has to be this" -- stupid case errors. I did get the aria-describedby set properly (see my second update) but it seems slightly convoluted to get the actual input element -- am I still missing something? – shannonsans Jan 17 '16 at 22:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.