Miral
Miral

Reputation: 13030

Repeated content (sub-template) in AngularJS

I have a template which contains (in part) exactly the same content repeated two or three times with minor changes to the bindings, eg:

<div class="xyz-state0" data-ng-hide="data.error || !data.states[0].name">
    <div class="xyz-content">
        <img data-ng-src="{{data.states[0].image}}" width="48" height="48">
        <span>{{data.states[0].name}}</span>
    </div>
</div>
<div class="xyz-state1" data-ng-hide="data.error || !data.states[1].name">
    <div class="xyz-content">
        <img data-ng-src="{{data.states[1].image}}" width="48" height="48">
        <span>{{data.states[1].name}}</span>
    </div>
</div>

How do I write this to avoid duplicating this HTML? This is specific to its parent view (it won't be used anywhere else) so creating a full-blown widget seems wrong.

Basically I want something similar to ngRepeat, but I can't use that for the following reasons:

I've managed to get a template fragment in a separate HTML file and included it with ngInclude, but I don't know how to get a single name in its new scope to refer to a specific item. My first attempt was this, which doesn't work:

<div class="xyz-state0" data-ng-include="'state.tpl.html'" data-ng-init="state=data.state[0]"></div>
<div class="xyz-state1" data-ng-include="'state.tpl.html'" data-ng-init="state=data.state[1]"></div>

I suspect I could probably do it with a custom controller, but that seems like a heavy solution too. What's the Right Way™?

Upvotes: 0

Views: 499

Answers (3)

Miral
Miral

Reputation: 13030

Just as an FYI, I later revisited this code and decided to do it like this instead: (I'm letting my original answer stand as that is more like what I was originally asking for, and they both seem reasonable in different cases.)

view.tpl.html

    <div data-ng-repeat="state in data.states" data-ng-if="!data.error"
         data-ng-class="state.class">
        <div class="xyz-content" data-ng-show="state.name">
            <img data-ng-src="{{state.image}}" width="48" height="48" />
            <span>{{state.name}}</span>
        </div>
    </div>

app.js

...
while ($scope.data.states.length < 2)
    $scope.data.states.push({});
$scope.data.states[0].class = 'xyz-state1';
$scope.data.states[1].class = 'xyz-state2';
...

I've done something similar for the other (3-item) case, except there as I wanted to rearrange the order of the items I added an order property for the desired order in the controller and then used data-ng-repeat="button in data.buttons|orderBy:'order'" in the view.

This does mean that a bit of view definitions (display order and CSS classes) have leaked into the controller, but I think the benefit to code clarity outweighs that.

Upvotes: 0

pfooti
pfooti

Reputation: 2704

This is pretty much a textbook case for a custom directive. Define a directive, and then you can do

<state ng-repeat="item in data.states" item="item">.

Alternatively, if a custom directive is too much overkill (depends on whether you'll be reusing that view component elsewhere, mainly), you could just put an ng-repeat on the entire div. The only real issue is the class="xyz-stateN" stuff, but I bet you could hoke that up with ng-class usage.

EDIT:

if you do an ng-repeat, you can just use the $index key (as long as you're always counting up from zero and the state class is the same as the index). Something like

<div ng-class="{{'xyz-state'+$index}}" ng-repeat="state in data.states" data-ng-hide="data.error || !state.name">
    <div class="xyz-content">
        <img data-ng-src="{{state.image}}" width="48" height="48">
        <span>{{state.name}}</span>
    </div>
</div>

Would probably work fine. All that said, it's almost always worth making a directive in my opinion. Code gets recycled all the time, plus you can be cautious with namespacing and modularizing if that makes you nervous.

Upvotes: 2

Miral
Miral

Reputation: 13030

Well, this seems to do the trick (thanks to pfooti for the hint). I'm still not entirely happy with it as the directive is registered globally, whereas I really only want it in this one place.

state.tpl.html:

<div class="xyz-content" data-ng-show="state.name">
    <img data-ng-src="{{state.image}}" width="48" height="48" />
    <span>{{state.name}}</span>
</div>

view.tpl.html:

    <div data-xyz-state="data.states[0]" class="xyz-state0"
         data-ng-hide="data.error"></div>
    <div data-xyz-state="data.states[1]" class="xyz-state1"
         data-ng-hide="data.error"></div>

app.js:

app.directive('xyzState', [function() {
    return {
        templateUrl: 'state.tpl.html',
        scope: {
            state: '=xyzState',
        },
    };
}]);

Interestingly it doesn't work if I try to declare the introducing element as <xyz-state ...> instead of <div data-xyz-state="" ...>, despite the docs saying that this ought to work too. I assume there's some sort of validation thing interfering here.

Upvotes: 0

Related Questions