Skip to main content Skip to footer

How to Create Simple Reports with AngularJS

Reporting is a common requirement for business applications. Many specialized tools—SSRS, FlexReport, Active Reports—are powerful and flexible, but have a learning curve associated with them.

If all you need is a simple report, you can accomplish that with a CollectionView, some templates in ng-repeat directives, and a PrintDocument:

  • The CollectionView is responsible for sorting, grouping, and filtering the data.
  • The templates and ng-repeat directives transform the data into documents, using the regular layout and styling features made available by HTML and CSS.
  • Finally, the PrintDocument class transforms the report into a print-friendly version and sends that to the browser’s printing infrastructure. The browser then takes over to provide features including print-preview (Chrome and Edge), page size and orientation, headers and footers, output device (including PDF output), etc.

View the Sample

Download Now!

This approach has some advantages and some limitations.

The main advantage is that by using the browser’s own render engine, you get the same rich layout and styling features provided by HTML and CSS. The CollectionView and PrintDocument classes are part of the Wijmo core (wijmo.min.js), so there’s nothing new to learn, and nothing extra to download.

The main disadvantage is that HTML is provides little control over page dimensions. Headers, footers, and margins are defined outside the report, between the user and the browser. CSS attributes provide some control over page breaks, though, so this is not as big a limitation as it may seem at first.

If you are looking for a more interactive data table, then check out FlexGrid, our Angular DataGrid component.

An example of a simple AngularJS report


We have created a sample application demonstrating the basic ideas behind creating simple reports. You can see it live, inspect the source code, or download the sample here:

The sample implements a few reports, all based on the same mechanism:

  • Load the report data into one or more CollectionView objects,
  • Define an HTML fragment that uses ng-repeat directives and templates to define the report content,
  • Render the report into a PrintDocument and print it.

Let’s go over the details of each step.

Loading the report data

The report data is typically downloaded from a server using a web service. If the data is available as an OData source, you can get it easily using an ODataCollectionView object. This is what we used in the sample, which is based on the traditional NorthWind database.

Here’s the code that loads the report data:

// load data for reports  
var url = 'http://services.odata.org/V4/Northwind/Northwind.svc';  
$scope.products = new wijmo.odata.ODataCollectionView(url, 'Products', {  
 groupDescriptions: [  
  new wijmo.collections.PropertyGroupDescription('ProductName',  
   function (item, propName) {  
    var value = item[propName];  
    return value[0].toUpperCase();  
   }  
  )  
 ],  
 sortDescriptions: ['ProductName'],  
 loaded: function (s, e) {  
  $scope.$apply();  
 }  
});  

This code loads the Products table into a CollectionView and groups the data by product initial. Notice how the groupDescriptions property is initialized with a PropertyGroupDescription object whose function returns the first letter in the product name.

When the CollectionView is loaded, in addition to the regular items collection containing the products, it will contain a groups collection containing CollectionViewGroup objects. Each group provides a name property containing the grouping value (in this case the product’s initial) and an items property that contains the products within the group. This is very useful for creating grouped reports.

$scope.categories = new wijmo.odata.ODataCollectionView(url, 'Categories', {  
 fields: ['CategoryID', 'CategoryName', 'Description'],  
 sortDescriptions: ['CategoryName'],  
 loaded: function (s, e) {  
  $scope.$apply();  
 }  
});  

This code loads the Categories table into a CollectionView and retrieves only three fields from the server. This is done to prevent downloading the images that are not used in our reports and are relatively large and slow to download.

The application repeats this pattern to load employees, customers, sales, and invoice data.

Note that the data is loaded asynchronously; when each CollectionView is finished loading, it calls $scope.$apply to update the view with the new data.

If your data is not available as an OData source, you can still get the data using regular HttpRequest calls and create CollectionView objects based on arrays of data objects.

Defining the report

Each report is defined by an HTML fragment. In our example, the reports are stored in the application’s “partials” folder.

Let’s start with a simple report called “productsByCategory”, which is based on two data sources: “categories” and “products”. This is the HTML that defines the report:

 <h2 style="background-color:#cccccc; padding:6px">
 Products By Category <span style="font-size:50%; font-style=italic">As of {{ today | date }}</span></h2>

 <div ng-repeat="category in categories.items" style="page-break-inside:avoid">

  <h2> {{category.CategoryName}}</h2>
  <div style="margin-left:12pt; columns:3">
    <!-- multi-column --> 
    <div ng-repeat="product in select(products, 'CategoryID', category.CategoryID)">  
      {{ $index + 1 }}) {{ product.ProductName }} 
    </div>
  </div>
</div> 

The report starts with a header that contains the title and today’s date. Notice how the report header uses the Angular filter called “date” to format the date value. Filters can also be used to format numbers and currencies.

That is followed by the report body, which has an ng-repeat directive that enumerates the categories. The ng-repeat directive is contained in a div element that uses a style attribute to set its CSS property page-break-inside to “avoid”. This causes the browser to keep categories together on a page if possible.

Within the category div, another ng-repeat directive enumerates the products within the current category and uses a “select” command to retrieve the list of products that belong to the current category. This inner div has the CSS property columns set to “3”, which causes the browser to render the div into three columns.

The output looks like this:

A three-column report

Now let’s look at another report, “employeeSalesByCountry”. Here’s the HTML that defines the report:

<!-- "classified" watermark -->

<div class="watermark">

 <img style="position:absolute; left:50%; top:40%; transform:translate(-50%, -50%)" src="../resources/confidential.png" />

</div>

The report starts with a watermark that shows a “confidential” image behind the report content on every page. The “watermark” class is used by these CSS rules:

.watermark {  
 position: fixed;  
 width: 100%;  
 height: 100%;  
}  
@media not print {
 .watermark {  
  display: none;  
 } 
}  

The CSS makes the watermark element fill the entire page and repeat for every page. It also make the watermark visible only on printed output and not on the screen.

Note that although the “position:fixed” setting should, in theory, cause the content to repeat on every page, not all browsers implement this correctly. Internet Explorer and Edge do it correctly; Chrome renders the watermark only on the first page. We hope this will be fixed soon.

After the watermark, we define the report header:

<!-- report header -->

<h1 style="background-color:#666; color:#fff; padding:10px">

 Employee Sales By Country

 <br/>

 <span style="font-size:50%; font-style:italic">

  Between {{ invoices.getAggregate('Min', 'OrderDate') | date }} and

      {{ invoices.getAggregate('Max', 'OrderDate') | date }}

 </span>

</h2>


The header contains a title and the dates reflected in the report. Notice how the date range is calculated directly from the data using the CollectionView’s getAggregate method.

Finally, the report body is defined as follows:

<!-- report body -->

<div ng-repeat="country in invoices.groups" style="page-break-inside:avoid">

 <div style="border-bottom:6pt solid #a0a0a0; display:flex" ng-init="countrySales = country.getAggregate('Sum', 'ExtendedPrice')">

  <div style="width:600px">
   {{ country.name }}
  </div>

  <div style="width:500px; text-align:right">
   {{ countrySales | currency }}
  </div>

 </div>

 <div ng-repeat="salesperson in country.groups" style="display:flex" ng-init="personSales = salesperson.getAggregate('Sum', 'ExtendedPrice')">
  <div style="width:600px; padding-left:0.5in">
   {{ salesperson.name }}
  </div>

  <div style="width:200px; text-align:right">
   {{ personSales / countrySales * 100 | number:0 }}%
  </div>

  <div style="width:300px; text-align:right">
   {{ personSales | currency }}
  </div>

 </div>

</div>


The outer div uses an ng-repeat directive to enumerate the countries (“invoices.groups”).

It computes and saves the total country sales using the getAggregate method to sum the “ExtendedPrice” property of the invoices and saves that value in a “countrySales” variable.

It renders the country name and total sales, and then uses another ng-repeat directive to enumerate the salespersons within that country. The inner ng-repeat lists the name of the salesperson and the sales made by that person as a percentage of the total sales for the current country and as a currency value.

The result looks like this:

Grouping and aggregation

These two report definitions illustrate the approach used in all other reports in the sample. They’re all based on ng-repeat directives that iterate through the data and standard HTML/CSS code that formats and lays out the reports.

Generating the reports

In our sample application, when a report is selected from the ComboBox, the corresponding view is loaded and AngularJS automatically renders it into the document.

Simply printing the document at this point generates a document containing the sample’s headers and other content that is visible, but not part of the report. Worse, the output shows the scrollbars in the container element, and the actual report is cropped.

This is where the PrintDocument class comes into play. The application controller defines a print method implemented as follows:

$scope.print = function () {  

 // create document  
 var doc = new wijmo.PrintDocument({  
  title: $scope.reports.currentItem.header  
 });   

 // add content to it  
 var view = document.querySelector('[ng-view]')  
 for (var i = 0; i < view.children.length; i++) {  
  doc.append(view.children[i]);  
 }  

 // and print it  
 doc.print();  
}  

Here’s what this method does.

  • It creates a PrintDocument object;
  • It sets its title property to the report name;
  • It adds the content by copying the content of the ng-view element that contains the report;
  • And finally, it calls the document’s print method, which sends the report to the browser for previewing and printing.

Useful reporting techniques

All of the reports in the sample use the same techniques to accomplish simple and common tasks. Some of these are especially useful when generating report-style output.

Aggregates

The CollectionView and CollectionViewGroup classes provide a getAggregate method that computes aggregates over the whole collection or group. These are useful for generating ranges or sums over groups as shown in the examples above.

For example, recall that our “employeeSalesByCountry” has a header that shows the report period. This is done using the getAggregate method:

<h1 style="background-color:#666; color:#fff; padding:10px">

 Employee Sales By Country

 <br/>

 <span style="font-size:50%; font-style:italic">

  Between {{ invoices.getAggregate('Min', 'OrderDate') | date }} and

      {{ invoices.getAggregate('Max', 'OrderDate') | date }}

 </span>

</h2>

In the same report, we calculate the total sales per country and store that value in a variable that can be used in the report and in later calculations:

<div style="border-bottom:6pt solid #a0a0a0; display:flex" ng-init="countrySales = country.getAggregate('Sum', 'ExtendedPrice')">

  <div style="width:600px">
   {{ country.name }}
  </div>

  <div style="width:500px; text-align:right">
   {{ countrySales | currency }}
  </div>

  ...

Page breaks

One of the main limitations in using the browser to generate documents is the lack of control over page size, orientation, and margins. But you do get some level of control over page breaks, which is very convenient to prevent page breaks within report sections.

This is done using the page-break-before, page-break-after, and page-break-inside CSS properties. These attributes have no effect on the screen, but they do affect printed output. Most of the reports in this sample have header sections with the page-break-inside attribute set to “avoid”, which results in groups that stay together on a page (if possible).

For example:

<div ng-repeat="salesperson in country.groups" style="page-break-inside:avoid" ng-init="personSales = salesperson.getAggregate('Sum', 'ExtendedPrice')">

 <div style="width:600px; padding-left:0.5in">
  {{ salesperson.name }}
 </div>

 <div style="width:200px; text-align:right">
  {{ personSales / countrySales * 100 | number:0 }}%
 </div>

 <div style="width:300px; text-align:right">
  {{ personSales | currency }}
 </div>

</div>


The page-break-* properties are being replaced by more generic break-* properties, which also handle column and region breaks and will be syntactically compatible with page-break-*. For the time being, however, we only have control over page breaks, and not columns or regions.

Watermarks and other repeating content

In addition to controlling page breaks, you can specify content that repeats on every page, such as a watermark, letterhead, header, or footer.

This is done by putting the repeating content in a div with the position property set to “fixed”. The HTML/CSS specification dictates that this type of element should be rendered on every page. You can use this setting and a CSS media query to generate sections that repeat on every page.

The “employeeSalesByCountry” report uses this technique to add a “Confidential” watermark image to every page on the report.

Unfortunately, there are some caveats:

  • Not all browsers implement position:fixed according to the spec. We mentioned earlier that Edge and IE 11 do. Chrome, however, renders the fixed content only on the first page.
  • Using this technique to add page headers and footers can be tricky because they have to be sized and positioned in a way that works with all page sizes and orientations.
  • Users may configure the browser to add its own page headers and footers, which may conflict with the custom report headers and footers.

Multiple columns

Creating reports with multi-column sections is easy using the CSS column-width, column-count, and columns properties, or with flexible boxes.

The column property allows you to specify a column width to be used within a div. The browser will automatically calculate how many columns fit within the div and how many rows are needed to render it. This simplifies creating responsive multi-column reports. You can also specify how many columns you want, in which case, the browser calculates the column width.

The flexible box approach is slightly more complicated, but offers even more flexibility. Just set the div’s display property to “flex”, and set the wrap, align, and justify properties to achieve the layout you want. For details about flexible boxes, please see this page:

https://css-tricks.com/snippets/css/a-guide-to-flexbox

The reports in the sample use both approaches. The “productsByCategory” report uses a div with columns set to three to render product names into three columns:

<div style="margin-left:12pt; columns:3"> <!-- multi-column -->

 <div ng-repeat="product in select(products, 'CategoryID', category.CategoryID)">
  {{ $index + 1 }}) {{ product.ProductName }}
 </div>

</div>


The “employeeSalesByCountry” report and several others use flex boxes to render headers aligned above the actual data:

<div style="border-bottom:6pt solid #a0a0a0; display:flex" ng-init="countrySales = country.getAggregate('Sum', 'ExtendedPrice')">

 <div style="width:600px">
  {{ country.name }}
 </div>

 <div style="width:500px; text-align:right">
  {{ countrySales | currency }}
 </div>

…


Borders

Borders are useful when defining reports because they’re always rendered in the printed output (unlike div backgrounds, which are sometimes omitted).

For example, the “salesChart” report uses this HTML to build a bar chart where each bar is a border on an element with a variable width:

<div ng-repeat="invoice in salesperson.items" style="display:flex; padding-right:6pt">

 <div style="width:600px">

  <div style="height:1em; margin-top:.25em; border-top:.6em solid #007200" ng-style="{width: invoice.ExtendedPrice * 100 / 18000 + '%' }">

   &nbsp;

  </div>

 </div>

…


In this case, setting the background-color would not work. The chart appears correctly on the screen, but it does not render on the print document.

Using Controls in Reports

You can use plain HTML in your report definitions, but you can also go beyond that and include other controls such as charts and gauges.

For example, the “salesByCategory” report in the sample contains this HTML:

<div ng-repeat="categorySales in productSales.groups">

 <div style="display:flex; font-weight:bold; font-size:200%">
  {{categorySales.CategoryName}}
 </div>

 <div style="display:flex; page-break-inside:avoid">

  <div>

   …

  </div>

  <wj-flex-chart 
   style="width:500px; height:350px; margin:12pt; border:4px solid #009be0"
   items-source="categorySales.items"
   binding-x="ProductName">

   <wj-flex-chart-series binding="ProductSales">

   </wj-flex-chart-series>

  </wj-flex-chart>

 </div>

</div>


The markup uses an ng-repeat to loop over the categories. For each category, it defines a flexible box with a table containing product names and sales on the left, and a FlexChart on the right.

The wj-flex-chart directive sets the items-source property of the chart to “categorySales.items”, which contains an array of objects with ProductName and ProductSales properties. We could easily customize the chart by adding more attributes to the markup.

Here is the result:

Display controls in a report

You can use any controls in your reports, including charts, linear and radial gauges, and bullet graphs.

Specialized Reporting Tools

The techniques discussed here can be very useful for generating relatively simple reports. But if your needs are more complex, then you should consider using specialized reporting tools. These tools provide advanced features like server-side rendering and caching, control over page headers and footers, cross-tabs, parameterized reports, and much more.

We’ll soon release the Wijmo ReportViewer control, which will work with server-side reporting tools including FlexReport, ActiveReports, and Microsoft Reporting Services.

To use the ReportViewer control, create the control and set a couple of properties as usual:

var viewer = new wijmo.viewer.ReportViewer('#flexViewer', {  
 serviceUrl: serviceUrl,  
 filePath: reportFilesCombo.value,  
 reportName: reportNamesCombo.value,  
});  

The viewer contacts the service, retrieves and displays the report in a UI element similar to the one in the Adobe PDF viewer.

Users will then be able navigate through the report using outline or thumbnail views, zoom, search, select report parameters, set page size and orientation, and save or print the results.

Stay tuned for more details about FlexReport and the ReportViewer control.

Conclusion

Browsers are amazing applications. They have many features that can be easily be overlooked, but are really useful in many real-world scenarios. Printing support is one of these.

You may need a specialized component for creating reports, but if you use Wijmo and AngularJS, you already have all the tools you need to generate basic reports quickly and efficiently.

And when you outgrow the basic capabilities already in your toolbox, rest assured knowing that Wijmo also supports advanced and flexible server-based reporting.

View the Sample

Download Now!

Bernardo de Castilho

comments powered by Disqus