Search code examples
javascriptractivejs

Composable Ractive js component


I'm working on a Ractive js component that represents a table. It works ok but recently I added some functionality: filters, status bar, left column checkboxes, table header. This table component is used throughout the application everywhere.

The problem is that in different places I need only some combination of these functions. In the current implementation I have a lot of ifs in the template and I want to get rid of them.

At the moment I want to use the decorator pattern but have no idea how to implement this especially with the fact that I need to mess with the core table template.

Table template:

{{#if status_bar}}
<table class="list-status">
    <tbody>
      <tr>
        <td><input type="checkbox" checked="{{.select_all}}"></td>
        <td>
          {{#if has_filters}}
            <span>Показать:</span>
            {{#each filters:i}}
              <button on-tap="@this.fire('activateFilter', event, i)" {{#if active}}class="active"{{/if}}>{{title}}</button>
            {{/each}}
          {{/if}}
          {{#if ready}}
            <span class="badge lists-badge">{{ collection.total_count }}</span>
            {{#if selected.length > 0}}
              <span class="badge lists-badge" style="background-color: red;">{{ selected.length }}</span>
            {{/if}}
          {{/if}}
        </td>
      </tr>
    </tbody>
  </table>
{{/if}}

<table class="{{class}}">
  {{#if header}}
    <thead>
      <tr>
        {{^hideSelectColumn}}
          <th></th>
        {{/hideSelectColumn}}
        {{#each titles}}
          <th class={{class}}>{{title}}</th>
        {{/each}}
      </tr>
    </thead>
  {{/if}}
  <tbody>
    {{#if ready}}
      {{#each diff_create:i}}
        <tr class="just-created">
          {{^hideSelectColumn}}
            <td class="{{firstColumnClass}}">
              <div style="position: relative;"></div>
            </td>
          {{/hideSelectColumn}}
          {{>pending}}
        </tr>
      {{/each}}
      {{#if diff_create.length > 0}}
        <tr>
          <td colspan={{column_count}}></td>
        </tr>
      {{/if}}
      {{#each page_models}}
        <tr {{#if mfd}} class='mfd' {{/if}}>
          {{^hideSelectColumn}}
            <td class="{{firstColumnClass}}">
              <input type="checkbox" class="list-checkbox" name='{{selected}}' value='{{@index}}'/>
              <div style="position: relative;"></div>
            </td>
          {{/hideSelectColumn}}
          {{>content}}
        </tr>
      {{/each}}
      {{^page_models}}
        <tr>
          <td style='text-align:center; font-weight: bold; font-size: 15px' colspan={{column_count}}>Пусто!</td>
        </tr>
      {{/}}
    {{/if}}
    {{^ready}}
      <tr>
        <td style='text-align:center; font-weight: bold; font-size: 15px' colspan={{column_count}}>Загрузка...</td>
      </tr>
    {{/ready}}
  </tbody>
</table>

<div class="pages">
  {{#if pages_more_than_one}}
    <ul class="pagination">
      {{#if has_prev_page}}
        <li class="arrow-left"><a on-tap='prevPage'><span class="icon-arrow-left4"></span></a></li>
      {{/if}}
      {{#each pages}}
        <li class='{{@index+1==current_page ? "active": ""}}'><a on-tap='@this.fire("goToPage", @index+1)'>{{@index+1}}</a></li>
      {{/each}}
      {{#if has_next_page}}
        <li class="arrow-right"><a on-tap='nextPage'><span class="icon-arrow-right8"></span></a></li>
      {{/if}}
    </ul>
  {{/if}}
  <div class="pages-count">
    <select class="form-control" value="{{collection.perpage}}">
      <option value='2'>2</option>
      <option value='5'>5</option>
      <option value='10'>10</option>
      <option value='15'>15</option>
      <option value='20'>20</option>
      <option value='25'>25</option>
    </select>&nbsp;
  </div>
</div>

Thanks in advance.


Solution

  • Ractive has named yields where you can define placeholders in your layout and let the consuming component define what goes in there. Think of it like a layouting mechanism.

    A simple example is a modal, where you have a header, body and footer which may be different for each instance. A modal component could be defined only as the layout of the modal (think empty bootstrap modal) and a bunch of yields resembling insertable positions.

    const Modal = Ractive.extend({
      template: `
        <div class="modal">
          <div class="modal__header">{{yield header }}</div>
          <div class="modal__body">{{yield body }}</div>
          <div class="modal__footer">{{yield footer }}</div>
        </div>
      `
    })
    

    On the consuming component, you use the layout component and define what goes in those positions. Note that the yield contents can be anything, even other components.

    const Page = Ractive.extend({
      components: { Modal },
      template: `
        <div class="page">
          <div class="page__stuff">
            ...
          </div>
    
          <Modal>
            {{#partial header }}
              This is the header
            {{/partial}}
    
            {{#partial body }}
              This is the body
            {{/partial}}
    
            {{#partial footer }}
              This is the footer
            {{/partial}}
          </Modal>
    
        </div>
      `
    });
    

    In your case, you can break up your table into components (i.e. filters, status bar, left column checkboxes, table header) and define a layout component. Then, whenever you need it, you use the layout component, stick into it whatever you want wherever you want.

    A more advanced feature is Anchors. Together with ractive.attachChild() and ractive.link(), you can attach components to anchors manually, on-the-fly. It's essentially how Ractive mounts components and binds data when it sees a component on the template. Except this time, the functionality is exposed on the API.