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:
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.
<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}">
</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>
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.
In your CCA constructor, just store the value of unique onto a convenient attribute on the component model. I always use unq thus:
self.unq = context.unique;
Step 2 - Generate IDs and FOR attributes
Now that we have the unq variable we can re-write the problem view using the attr:{} knockout binding, thus:
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.
<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'+unq}">Name:</label>
</div>
<div class="oj-flex-item">
<input data-bind="attr:{id:'name'+unq},
ojComponent: {component: 'ojInputText',
value:name,
required: true}">
</div>
</div>
<div class="oj-flex">
<div class="oj-flex-item">
<label data-bind="attr:{for:'type'+unq}">Type:</label>
</div>
<div class="oj-flex-item">
<select data-bind="attr:{id:'type'+unq},
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>
CCA Series Index
- Introduction
- Your First Composite Component - A Tutorial
- Composite Conventions and Standards
- Attributes, Properties and Data
- Events
- Methods
- The Lifecycle
- Slotting Part 1
- Slotting Part 2
- Custom Property Parsing
- Metadata Extensibility
- Advanced Loader Scripts
- Deferred UI Loading
- Using ojModule in CCAs
- Language Support
- CCAs in Form Layouts
- Element IDs in CCA