1

I want to create a new row every 6 iterations of a loop, but because I have a div tag in an if with no closing div, I'm getting an error.

What is the correct, "Angular 19" way to do this?

<div class='container'>
  @for(role of userRoles; track role.id){
    @if(+role.id % 6 === 0){
      <div class="row">
    }                        <<<<<< Error here <<<<<<<
    <div class="col-2">
      {{role.name}}
    </div>
    @if(+role.id % 6 === 5){
      </div>
    }
  }
</div>

The error is:

Unexpected closing block. The block may have been closed earlier. If you meant to write the } character, you should use the "}" HTML entity instead.ngtsc(-995002)

4 Answers 4

1

Angular expects every opening HTML tag element to have a closing tag element (excluding self closing tags <img src="..." />).

So we need to split the array at the nth element and then loop through to create the row in between nth elements.

splitAtN(userRoles: any[], n: number) {
  const output = [],
    i = 0;
  while (userRoles.length) {
    output.push(userRoles.slice(i == 0 ? 0 : i + 1, i + n));
    userRoles = userRoles.slice(i + n);
  }
  return output;
}

Full Code:

import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';

@Component({
  selector: 'app-root',
  template: `
    <div class='container'>
      @for(userRolesNth of userRolesTransformed; track $index){
        <div class="row">
        @for(role of userRolesNth; track role.id) {
          <div class="col-2">
            {{role.name}}
          </div>
        }
        </div>
      }
    </div>
  `,
})
export class App {
  userRoles = [
    { name: '1', id: 1 },
    { name: '2', id: 2 },
    { name: '3', id: 3 },
    { name: '4', id: 4 },
    { name: '5', id: 5 },
    { name: '6', id: 6 },
    { name: '7', id: 7 },
    { name: '8', id: 8 },
    { name: '9', id: 9 },
    { name: '10', id: 10 },
    { name: '11', id: 11 },
    { name: '12', id: 12 },
    { name: '13', id: 13 },
    { name: '14', id: 14 },
  ];

  userRolesTransformed: any[] = [];

  ngOnInit() {
    this.userRolesTransformed = this.splitAtN(this.userRoles, 5);
  }

    splitAtN(userRoles: any[], n: number) {
      const output = [],
        i = 0;
      while (userRoles.length) {
        output.push(userRoles.slice(i == 0 ? 0 : i + 1, i + n));
        userRoles = userRoles.slice(i + n);
      }
      return output;
    }
}

bootstrapApplication(App);

Stackblitz Demo

Sign up to request clarification or add additional context in comments.

1 Comment

Yes this is what I ended up doing. It's unfortunate that when they were adding in the for and if directives they didn't think to include an intuitive and fool-proof way to accomplish this.
1

They way you're going about this isn't going to work. I don't know why Angular this way, but I guess it's to keep developers from shooting themselves in the foot. For example, your implementation will forget the closing tag if userRoles's length isn't a multiple of 6.

So either embrace a CSS solution that will work with your data as it is or bind to data that conforms to your view.

CSS Solution (Preferred)

It looks like you're using Bootstrap. If so, then the easiest way accommodate your desired layout is to use grid instead of rows. If it's not Bootstrap you can still do something similar since most design frameworks support grid layouts.

The following will create a six column grid:

<div class="grid">
  @for(role of userRoles; track role.id) {
    <div class="g-col-2">
      {{role.name}}
    </div>
  }
</div>

View Binding Solution

If you must use outer elements with row classes, then conform your view bound data to fit the structure of your template:

// Chunk the array into sub arrays of 6 elements.
userRoleRows = userRoles.reduce((acc, curr, index) => {
  (acc[Math.floor(index / 6)] ??= []).push(curr);
  return acc;
}, [] as UserRole[][]);
<div class="container">
  @for (row of userRoleRows; track row) {
    <div class="row">
       @for (role of row; track.role.id) {
          <div class="col-2">
            {{role.name}}
         </div>
       }
    </div>
  }
</div>

Conclusion

If you can, use the CSS grid solution as this will allow you to do responsive layouts easily and you can keep a design concern out of your component code.

Comments

1

If you want to add a conditional class. This is the cleanest way I can think of:

<div class='container'>
    @for(role of userRoles; track role.id; ; let  i = $index) {
      <div class="col-2" [class.row]="i%6 === 0">
        {{role.name}}
      </div>
  }
</div>

If the double div is required and it must be template only: (performance wise not the best)

<div class='container'>
    @for(_ of userRoles; track _.id; let  i = $index) {
      @if(i%6 === 0){
        <div class="row">
          @for(role of userRoles.slice(i, i + 6); track role.id){
            <div class="col-2">
              {{role.name}}
            </div>
          }
        </div>
      }
    }
</div>

Comments

0

I would try something like this:

  1. First in the component's .ts file group the roles in groups by 6 elements

    userRoleGroups = [];
    
    groupUserRoles() 
    {
       let i = 0;
       while(i < userRoles.length) {
          this.userRoleGroups.push(userRoles.slice(i, i + 6));
          i += 6;
       }
    }
    
  2. Then in the component's template iterate over the groups first, add the row div for the current group and then inside the row div iterate over each role of the current role group.

    <div class='container'>
      @for(roleGroup of userRoleGroups; track $index)
      {  
          <div class="row">
              @for(role of roleGroup; track role.id) 
              {
                 <div class="row">
                     <div class="col-2">
                         {{role.name}}
                      </div>
                 </div>              
               }
          </div>
        }
    </div>
    

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.