I have a small app in which I receive a question with some hidden words to be written down like this:
The {0} {1} {2} his {3} off
When this string is received, each {x}
string has to be substituted with an input that the user will fill with the correct answer. So for that, I created this code:
HTML part
<div *ngFor="let question of questionsArray">
---- some stuff ----
<div [innerHTML]="createQuestion(question)"></div>
---- some stuff ----
</div>
Typescript function:
createQuestion(question: string): SafeHtml {
let innerHtml = '';
let words = question.split(' ');
for (let index = 0; index < words.length; index++) {
const element = words[index];
if (element.indexOf('{') >= 0) {
innerHtml += '<input type="text" name="test"></input>';
} else {
innerHtml += element;
}
}
return this.sanitizer.bypassSecurityTrustHtml(innerHtml);
}
I also added the DomSanitizer
in the constructor like this:
constructor(private sanitizer: DomSanitizer) {}
It works fine and draws inputs like this:
But I can't write anything on the input. I guess that maybe the byPassSecurityHtml
might not be working because I didn't use any Pipe
as suggested here. But, as I need it to be created in a dynamic way as it needs to be called foreach question in my DOM, I can't figure out how to use it correctly...
Can anybody give me a hand with this?
The problem with DOM strings is that even though they are rendered by the browser, Angular does not see them as part of the template for view binding. The best approach to this problem is to use an array which defines how the template should be rendered like so:
createQuestion(question: string) {
const template = question.match(/[A-Za-z]+|{\d}/g) // <-- [ 'The', '{0}', '{1}', '{2}', 'his', '{3}', 'off' ]
.map(match => match[0] === '{' ? { type: 'input', value: ''}
: { type: 'string', value: match })
return template;
}
The createQuestion
method accepts a template string and uses a regular expression to split it into parts in the form [ 'The', '{0}', '{1}', '{2}', 'his', '{3}', 'off' ]
which I then pass into a map method which generates a uniform object for each part. Any part that has the string '{' is considered to be a placeholder for input so it gets turned into the form { type: 'input', value: '' }
any text gets turned into the form { type: 'text', value: 'text value here' }
so that we can later loop through this array and use *ngIf to conditionally render either text or an input.
This is the template that's generated for the exemplary string you have provided.
template = [
{ type: 'text', value: 'The' },
{ type: 'input', value: '' }
{ type: 'input', value: '' }
{ type: 'input', value: '' }
{ type: 'text', value: 'his' }
{ type: 'input', value: '' }
{ type: 'text', value: 'off' }
]
With this template you can create a value bound angular template like so,
<div *ngFor="let template of questionsArray.map(q => createQuestion(q))">
---- some stuff ----
<div *ngFor="let section of template">
<input *ngIf="section.type === 'input'" ([ngModel])="section.value" />
<span *ngIf="section.type === 'text'">{{ section.value }}</span>
</div>
---- some stuff ----
</div>
The outer *ngFor
directive loops through all the different questions that have been transformed into templates with .map(q => createQuestion(q))
. The inner *ngFor
directive loops through each section of the template and generates either a span
or an input
based on the type
property of each section. If the type is text
, a span is displayed. If the type is input
and input is displayed with ngModel binding to the value
property.