X

Technical Articles relating to Oracle Development Tools and Frameworks

  • JET
    February 22, 2017

JET Custom Components XVII - Beware the IDs

Duncan Mills
Architect

Introduction

When developing Composite Components, we need to be careful about our element IDs, particularly when multiple instances of the component will be used on the same page. This article explores one such use case in relation to labels and input fields in form style layouts

Background

In the last article I discussed how you can use a Composite Component for input within the context of an oj-form layout. This article looks at forms in a slightly different way, specifically implementing a form layout within a Composite Component.

What's the Problem?

On the face of it there is no problem here. You can build a Composite Component that lays out a series of fields using standard JET form layout styles, for example, this might be the view for my component:


<div class="oj-form-layout">
  <div class="oj-form oj-sm-odd-cols-12 oj-md-odd-cols-4 oj-md-labels-inline">
    <div class="oj-flex">
      <div class="oj-flex-item">
        <label for="name">Name:</label>
      </div>
      <div class="oj-flex-item">
        <input id="name" data-bind="ojComponent: {component: 'ojInputText',
                                                   value:name,
                                                   required: true}"></input>
      </div>
    </div>
    <div class="oj-flex">
      <div class="oj-flex-item">
        <label for="type">Type:</label>
      </div>
      <div class="oj-flex-item">
        <select id="type" data-bind="ojComponent: {component: 'ojSelect',
                                                   value: type,
                                                   required:true}">
          <option value="string">string</option>
          <option value="boolean">boolean</option>
          <option value="number">number</option>
          <option value="object">object</option>
          <option value="array">array</option>
          <option value="function">function</option>
        </select>
      </div>
    </div>
  </div>
</div>

That all looks OK right? - we have the label tag associated with the relevant input component using the matched ids referenced by the for attribute of the label to the id attribute of the matching input.

Indeed this example with work perfectly when there is only one instance of the CCA in the view. If, however, you have two instances running at once, you'll find that the layout of one of them is all messed up. In the above case, this would only be noticeable on the ojSelect field, its label would loose all styling and not show the required indicator.

What's going on?

Well, as you might have guessed, the problem here is down to the ids that we are referencing to manage the label to input relationship. If I have two instances of this CCA on the same screen (even if one is hidden in a dialog) I now have two fields with the id of name and two with the id of type. As you will be aware. Multiple DOM elements with the same ID are going to cause some degree of problem and in this case it manifests as a broken layout.

The Fix

Once you appreciate the risk for elements with an assigned id, the pattern that you can use to address it is very simple and should be used as a matter of course within your CCAs, even if you think that there will only be one instance.

Step 1 - Get an Instance Identifier

Recall from the article on lifecycle (No. VII in this series, linked below), that the context object that is passed into our CCA constructor includes an attribute called unique. This use-case is exactly what it is for. You can cache this value for programatic use. In your CCA constructor, just store the value of unique onto a convenient attribute on the component model. I always use unique thus:

self.unique = context.unique;

However, most of the time you will actually need this value in the HTML template, and the framework provides a pre-canned object for you in the form of $unique.  This can be used within your template where-ever you need to create a unique ID or ID reference.

Step 2 - Generate IDs and FOR attributes

Now that we know we have  the $unique variable we can re-write the problem view using the attr:{} knockout binding, thus:


<div class="oj-form-layout">
  <div class="oj-form oj-sm-odd-cols-12 oj-md-odd-cols-4 oj-md-labels-inline">
    <div class="oj-flex">
      <div class="oj-flex-item">
        <label data-bind="attr:{for:'name'+$unique}">Name:</label>
      </div>
      <div class="oj-flex-item">
        <input data-bind="attr:{id:'name'+$unique},
                     ojComponent: {component: 'ojInputText',
                                   value:name,
                                   required: true}"></input>
        </div>
      </div>
      <div class="oj-flex">
        <div class="oj-flex-item">
          <label data-bind="attr:{for:'type'+$unique}">Type:</label>
        </div>
        <div class="oj-flex-item">
          <select data-bind="attr:{id:'type'+$unique},
                              ojComponent: {component: 'ojSelect',
                                            value: type,
                                            required:true}">
          <option value="string">string</option>
          <option value="boolean">boolean</option>
          <option value="number">number</option>
          <option value="object">object</option>
          <option value="array">array</option>
          <option value="function">function</option>
        </select>
      </div>
    </div>
  </div>
</div>

With this minor change to the code, each instance of the CCA will now have a unique id = for pair generated. One point to note, however, is that this unique id may change from use to use of the same CCA instance. So if your CCA viewModel also wants to manipulate its own DOM or attach listeners based on a specific id, then again you should use the context.unique value to ensure that you get the correct element. You overall goal, however, should be to assign explicit ids to as few a number of elements that you can get away with. Selecting by class may be a better strategy for many DOM manipulation cases.

Step 2 - Generate IDs and FOR attributes (For JET 4 and above)

I thought that it was worth updating to this article to cover JET 4.0 and the new web-component syntax, just to keep it up to date and relevant.  There are are actually two important things to note:

  1. How we can inject a dynamic ID when using web component syntax (i.e. without data-bind="attr:{...})
  2. Taking advantage of the new uniqueId attribute on the context

Let's just talk about the second of these -  uniqueId first. Recall that earlier in this article I talked about generating the IDs for the elements within the component using $unique or context.unique as a part of a generated ID, so in the example above we used type+$unique, which would generate to something like "type_composite0".  This is fine but it has one drawback that particularly effects those writing QA tests.  This is that the generated ID is not stable, e.g. on one run of the page you might end up with type_composite0 but on a second, type_composite1.  So the IDs will be unique, but sadly not guaranteed to be stable.  The new uniqueId value on the context addresses it.  It acts as a proxy for whatever ID you assigned to the CCA instance itself, so if you had embedded the following inside of your page <my-cca id="c1">  then context.uniqueId (and $uniqueId) will contain the value "c1". This value of course will be stable and so QA test writers can rejoice. Note that if you use uniqueId within a CCA that does not have a formal ID assigned by the consumer then it will inherit the same value as context.unique, so things are guaranteed to keep working. 

So onto updating our testcase above to use the new element syntax.  Rather than using data-bind="" syntax to set the IDs we can use the special attribute colon syntax which allows you to assign expressions to certain of the base HTMLElement attributes (see this page in the cookbook for more) . In this case we are interested in :for and :id.

Here's the updated code, note the use of both the colon syntax and $uniqueId

<div class="oj-form-layout">
  <div class="oj-form oj-sm-odd-cols-12 oj-md-odd-cols-4 oj-md-labels-inline">
    <div class="oj-flex">
      <div class="oj-flex-item">
        <oj-label :for="[['name_'+$uniqueId]]">Name:</oj-label>
      </div>
      <div class="oj-flex-item">
        <oj-input-text :id="[['name_'+$uniqueId]]" value="{{name}}" required="true"></oj-input-text>
        </div>
      </div>
  </div>
  ...
</div>

JET Custom Component Series

If you've just arrived at Custom JET Components and would like to learn more, then you can access the whole series of articles on the topic from the Custom JET Component Architecture Learning Path

Be the first to comment

Comments ( 0 )
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.Captcha