Search code examples
javascripttemplatesscopehandlebars.js

Acessing variable outside the scope in handlebars


I have a handlebars template, that looks like this:

{{#each leagues}}
   <tr>
    <td>{{leagueName}}</td>
    <td>{{season}}</td>
    <td>
<form action="/groups/{{../id}}/teams/{{id}}/leagues/{{leagueId}}/season/{{season}}" method="POST">
    {{#if ../groups}}
    <select name="group" class="form-select">
        {{#each ../groups}}
         <option value="{{id}}">{{name}}</option>
        {{/each}}
    </select>
    <input type="submit" value="Add" class="btn btn-primary mt-2">
    {{else}}
      <div class="d-flex align-items-center">
         <p class="text-danger mb-0">No groups available</p>
         <a href="/createGroup" class="btn btn-secondary ms-2">Create Group</a>
      </div>
     {{/if}}
</form>
</td>
</tr>
{{/each}}

My issue is that when I do the post, I want to the action URL to have both the id of the league, and the id of the group. Both are called "id". When I do {{../id}} to access the group "id" that is outside the scope it still doesn't work.

This way, I'm getting, for example:

Cannot POST /groups//teams/123/leagues/321/season/1904

The group "id" is not getting set in the form action.

I'm passing the data to the template like this:

    resp.render("leaguesByTeam", { leagues, groups })

Where the groups have the following format:

[
  { id: 0, userId: 0, name: 'g1', description: 'g1 desc', teams: [] },
  { id: 1, userId: 0, name: 'g2', description: 'g2 desc', teams: [] }
]

If useful, leagues have the following format:

{
    id: '211',
    leagueId: 26,
    leagueName: 'International Champions Cup',
    season: 2018
  },
  {
    id: '211',
    leagueId: 3,
    leagueName: 'UEFA Europa League',
    season: 2018
  },

The goal I have in mind is to choose the league, and add it to the group I select in the dropdown menu. So I want the "id" from the group I selected, added to the URI, once I press the add button.


Solution

  • What you are trying to do is not going to work without some client-side JavaScript.

    The action attribute on each of your rendered forms - the endpoint URL to which the form data will be POSTed - is computed before the user has even loaded the document, let alone selected a group.

    You require a way to dynamically build the action URL whenever the user changes the selected group via the <select> menu.

    This is where the client-side JavaScript will come in. You will need to loop through each <form> element on your page (one for each league) and bind a submit event listener to it. In the callback to the listener, you will need a way to get all of the pieces (the id values) that you will use to compose your action URL. I think a clean way to do this would be to use hidden <input> elements for each id (league, team and season).

    You would then dynamically set the value of the action of the submitting form, right before it gets POSTed.

    The code would look something like:

    const rawTemplate = document.getElementById('Template').innerHTML;
    const template = Handlebars.compile(rawTemplate);
    
    const data = {
        groups: [
         {
            id: 0,
          userId: 0,
          name: 'g1',
          description: 'g1 desc',
          teams: []
        },
        {
            id: 1,
          userId: 0,
          name: 'g2',
          description: 'g2 desc',
          teams: []
        }
      ],
        leagues: [
        {
          id: '211',
          leagueId: 26,
          leagueName: 'International Champions Cup',
          season: 2018
        },
        {
          id: '211',
          leagueId: 3,
          leagueName: 'UEFA Europa League',
          season: 2018
        }
      ]
    }
    
    const output = template(data);
    
    document.getElementById('Output').innerHTML = output;
    
    // dynamic form handling to compose and set the action
    // when the form is submitted
    const forms = document.querySelectorAll('form');
    
    forms.forEach(form => {
        form.addEventListener('submit', (event) => {    
        const theForm = event.target;
        const teamId = theForm.querySelector('input[name="teamId"]').value;
        const leagueId = theForm.querySelector('input[name="leagueId"]').value;
        const season = theForm.querySelector('input[name="season"]').value;
        const groupId = theForm.querySelector('select[name="group"]').value;
        
        
        const action = `/groups/${groupId}/teams/${teamId}/leagues/${leagueId}/season/${season}`;
        
        theForm.setAttribute('action', action);
        
        // remove the following lines, they are for demo purposes
        event.preventDefault();
        document.getElementById('Action').innerText = action;
      });
    });
    <script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.7.8/handlebars.min.js"></script>
    
    <script id="Template" type="text">
      {{#each leagues}}
         <tr>
          <td>{{leagueName}}</td>
          <td>{{season}}</td>
          <td>
            <form method="POST">
              <!-- hidden inputs to store action variables -->
              <input name="teamId" type="hidden" value="{{id}}">
              <input name="leagueId" type="hidden" value="{{leagueId}}">
              <input name="season" type="hidden" value="{{season}}">
            
              {{#if ../groups}}
                <select name="group" class="form-select">
                  {{#each ../groups}}
                    <option value="{{id}}">{{name}}</option>
                  {{/each}}
                </select>
                <input type="submit" value="Add" class="btn btn-primary mt-2">
              {{else}}
                <div class="d-flex align-items-center">
                  <p class="text-danger mb-0">No groups available</p>
                  <a href="/createGroup" class="btn btn-secondary ms-2">Create Group</a>
                </div>
              {{/if}}
            </form>
          </td>
        </tr>
      {{/each}}
    </script>
    
    <table id="Output"></table>
    
    <pre id="Action"><pre>