HTML template filled in server-side and updated client-side
Asked Answered
M

4

13

I have a webpage with dynamic content. Let's say it's a product page. When the user goes directly to example.com/product/123 I want to render my product template on the server and send html to the browser. However, when the user later clicks a link to /product/555 I'd like to use JavaScript to update the template on the client-side.

I'd like to use something like Knockout.js or Angularjs, but I don't see how I can pre-populate those templates with some initial data on the server and still have a functioning template on the client. i.e. If my Angular template is this:

<ul>
    <li ng-repeat="feature in features">
      {{feature.title}}
      <p>{{feature.description}}</p>
    </li>
</ul>

When the user goes directly to the URL, I need something that still works as an Angular template, but is filled in with the html for the current product. Obviously this doesn't work:

<ul>
    <li ng-repeat="feature in features">Hello
      <p>This feature was rendered server-side</p>
    </li>
    <li>Asdf <p>These are stuck here now since angular won't replace them when
       it updates.... </p></li>
</ul>

It seems like my only option is to send the server-rendered html to the browser along with a separate matching template...?

In that case, I'd like to avoid writing every template twice. Which means I need to either switch to JavaScript for my server language (which I would not be happy about) or choose a template language that compiles to both Java and JavaScript and then find a way to hack it into the Play Framework (which is what I'm currently using.)

Does anyone have any advice?

Mesoblast answered 7/8, 2012 at 2:57 Comment(0)
T
7

If you would really like to have an initial value stored in an area before Angular activates- you can use the ng-bind attribute rather than {{bound strings}}, from your example:

<ul>
    <li ng-repeat="feature in features">
        <div ng-bind="feature.title">Hello</div>
        <p ng-bind="feature.description">This feature was rendered server-side but can be updated once angular activates</p>
    </li>
</ul>

I'm not sure where this would come in handy, but you'll also want to include the initial data-set as part of a script tag in the document, so that when angular DOES activate it doesn't wipe out the displayed information with nulls.

Edit: (As requested by commenters)

Alternatively, you could make an ng-repeat at the top of the list, have it configured to fill out based on the 'features' list itself. Following that ng-repeat element, have non-ng-repeat elements which have an ng-hide attribute with the setting ng-hide="features", if Angular loads, all the elements from the original server-provided list hide themselves, and the angular list jumps into existence. No hacky modifications to Angular, and no fiddling with the direct ng-bind attribute.

As a side note, you might still want to send a piece of script capable of reading that initial server-element for its data to feed it into angular before angular synchronizes if you want to avoid a blink where angular clears the data while waiting for a request for the same data from the server.

Tutto answered 7/8, 2012 at 4:41 Comment(5)
I'm upvoting this since it seems to be the most promising answer so far. I still don't see how to handle the case where the server renders several list items and then Angular replaces them later. I guess I could always fork Angular and add that feature....Mesoblast
@Mesoblast Oh, idea! Make an ng-repeat at the top of the list, have it configured to fill out based on the 'features' list itself. Following that ng-repeat element, have non-ng-repeat elements which have an ng-hide attribute with the setting ng-hide="features", if Angular loads, all the elements from the original server-provided list hide themselves, and the angular list jumps into existence. No hacky modifications to Angular, and no fiddling with the direct ng-bind attribute.Tutto
Awesome, I think that's what I'll go with. It's still not perfect but it seems like the best I'm going to do short of modifying angular or knockout. Honestly, I'm surprised that there isn't a framework that addresses this problem.Mesoblast
There are at least a handful of (very early-stage) app frameworks that are attempting to address this very problem. Derby has a working demo; Flatiron has stated aspirations to fully "isomorphic" client/server code and looks promising. You might also check out Meteor, which strongly prefers to render client-side but has specific server-side support for Google's ajax crawling API. [Future readers: this whole area is changing rapidly, so this comment is probably out of date.]Endogenous
@DessixMachina - @takteek's question is exactly what I was looking for, and I think your suggestion to use ng-hide is really promising. Could you perhaps put move that suggestion in the body of your answer?Distrustful
C
2

I've only used Knockout, not Angular, but a seemingly very common approach which I use is to have the initial state of your data rendered into the page markup as JSON, and on DOM ready use that to build your initial Javascript view model, then apply the Knockout bindings to build the UI. So the UI gets built client side even for an item such as your product which already exists on the server. This means the very same templates can be invoked both for the initial UI creation and when you add something client side, like a sub-product with its own view model and template. Is this an option for you?

Edit: in case I misunderstood your requirements, the approach I'm talking about is detailed more in this question: KnockoutJS duplicating data overhead

Cretaceous answered 7/8, 2012 at 4:26 Comment(4)
Unfortunately, no. The reason for the server-side rendering is so the URL is search engine crawable.Mesoblast
For getting your site to be search-engine crawlable, you can go with Google's instructions on the subject: developers.google.com/webmasters/ajax-crawling/docs/…Tutto
@DessixMachina On supported browsers I'm using the history API so my URLs won't have hash fragments.Mesoblast
@Mesoblast Take a look at my above answer- it should be sufficient for your needs, as it includes the HTML before the javascript even activates.Tutto
D
1

One option in AngularJS might be to use a directive that copies values rendered on the server into the model and have subsequent actions retrieve data via JavaScript.

I've used the method described here in an ASP.NET WebForms application to pre-populate my model via hidden values from the server on the first request. Per the discussion this breaks from the Angular way but it is possible.

Here is example of the html:

<input type="hidden" ng-model="modelToCopyTo" copy-to-model value='"this was set server side"' />

JavaScript:

var directiveModule = angular.module('customDirectives', []);

directiveModule.directive('copyToModel', function ($parse) {
    return function (scope, element, attrs) {
        $parse(attrs.ngModel).assign(scope, JSON.parse(attrs.value));
    }
});
Diversification answered 7/8, 2012 at 4:26 Comment(2)
This is an interesting idea but I'm not sure how it solves the problem in my example with the repeating element. If the server populates a list with 3 <li>s, I don't see how I can tell Angular to replace those 3 elements when the viewmodel updates later.Mesoblast
The value for the hidden input could be the JSON for $scope.features similar to Tom Hall's suggestion and once it was copied to the model the <li>s should populate. We didn't realize you wanted it completely rendered for search engine crawling so this won't really help.. the main reason I used this method was looking to avoid the empty form and delay while making the ajax call to get the initial data.. since it was hitting the backend already might as well populate the data without an extra roundtrip.Diversification
E
0

I don't know of a nice techiqunue for doing this, but this is something I've settled on for the time being in a rails application I'm building.

You start by initialising your template with seed data using ng-init.

<ul ng-init="features = <%= features.to_json %>">
    <li ng-repeat="feature in features">
      {{feature.title}}
      <p>{{feature.description}}</p>
    </li>
</ul>

Then you render the seed data twice. Once from the server and again once Angular has bootstrapped your application. When the application is bootstrapped, Angular will hide the initial seed data leaving only the angularized template.

It's important that you use ng-cloak to hide the angular template before it's bootstrapped.

<ul ng-hide="true">
  <% features.each do |feature| %>
    <li>
      <%= feature.title %>
      <p><%= feature.description =></p>
    </li>
  <% end %>
</ul>
<ul ng-hide="false" ng-cloak ng-init="features = <%= features.to_json %>">
    <li ng-repeat="feature in features">
      {{feature.title}}
      <p>{{feature.description}}</p>
    </li>
</ul>

It doesn't scale with large templates, you're duplicating markup, but at least you won't get that flickering while Angular is bootstrapping your app.

Ideally I'd like to be able to re-use the same template on the server as on the client. Something like mustache comes to mind. Obviously the trick would be implementing angular's directives and flow control. Not an easy job.

Emergency answered 10/4, 2013 at 2:32 Comment(1)
How do you do please whith your solution when a ng-include is used inside a ng-repeat ?Brynnbrynna

© 2022 - 2024 — McMap. All rights reserved.