0

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:

enter image description here

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?

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Sonhja
  • 8,230
  • 20
  • 73
  • 131

3 Answers3

1

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.

Avin Kavish
  • 8,317
  • 1
  • 21
  • 36
  • Is there any part that's unclear? I'll add some text – Avin Kavish Jun 03 '19 at 18:50
  • It's because of the types (mine was a complex object). But I think your solution will be the good one for me. Let me try it again :) – Sonhja Jun 04 '19 at 07:29
  • Thanks Alvin! I accepted your answer and posted my modified solution. But your answer was the most aproximate to my problem and with your answer I could make it the most angularish possible ^^ So.. thanks a lot! – Sonhja Jun 04 '19 at 07:56
0

This is not how you're supposed to use Angular.

In Angular, you don't manipulate the DOM yourself. You're supposed to let the framework handle that for you.

For your issue, I think the best solution would be to use a pipe on your original string, as to not modify it.

Here is some stackblitz so that you can see it in action

  • But I have a completely dynamic list of questions that might have different amount of inputs to be introduced... I know I'm not supposed to modify the DOM but... how can I do that in another way? – Sonhja Jun 03 '19 at 17:24
  • @Sonhja by making exactly what I just did ? I've just given you the way to do it without modifying the Dom, now just adapt it to your case ... –  Jun 03 '19 at 18:13
  • I used that method before when only a question had to be answered. But now... we don't know the exact number of questions to solve. It's dynamic and comes from a non-controlled database :( So that's my real problem. The way you used it, I made it exactly the same in another section that the user needs to answer only one. But, what about if we have a variable number of questions to answer? How to create the DOM to define the desired number of inputs? – Sonhja Jun 03 '19 at 18:41
  • Ever heard of ngFor ? –  Jun 03 '19 at 18:48
  • This answer doesn't address the question of generating a dynamic form based on a user input template. – Avin Kavish Jun 03 '19 at 18:52
  • @AvinKavish no it doesn't, but people are stubborn, and even if you give them the best practices, they will choose not to listen just because. –  Jun 03 '19 at 19:13
  • @Maryannah of course I did! And as I said, I used this solution before when only an input had to be shown. Please, I hope guys you to be respectful as I'm trying to be the most angularish possible considering I've been working a few time with it and need to do something that dunno how to reach it being as much angularish as I can! So please... respect. I found a solution based on Maryannahs purpose. I post it! And I like it because is as angularish as it can be based on such a dynamic and non controlled environment. – Sonhja Jun 04 '19 at 07:48
  • @Sonhja repeating it over & over neither makes it true, nor makes me believe it. Look, I remain respectful, to an extent. There is objectively no limit of inputs to this answer, this is just an excuse you're using for whatever reason you have. I don't mind that, as I said people (including me) are stubborn (which isn't disrespectful by the way). You don't want to do that ? Sure, suit yourself, I don't mind. But say it explicitly and stop making up excuses as to why you can't use it. I see this everyday on SOF and honestly, it's kind of annoying. –  Jun 04 '19 at 07:52
0

Based on @Avin Kavish purpose, I found this simplest solution:

Typescript part

createQuestion(question: QuestionDto): Array<string> {
    let words = question.correctAnswer.split(' ');
    return words;
}

Return a string array with all separated elements. It returns this:

 ["The", "{0}", "{1}", "{2}", "his", "{3}", "off"]

HTML part In the UI part I check the content array in order to decide wether to draw an input or the text.

<div *ngFor="let question of questionsArray">
    <div *ngFor="let word of createQuestion(question); index as i">
       <input *ngIf="word.includes('{'); else elseBlock"
           id="word-{{i}}"
           class="form-control"
           type="text" />
           <ng-template #elseBlock>{{ word }}</ng-template>
    </div> 
 </div>
Sonhja
  • 8,230
  • 20
  • 73
  • 131