Skip navigation
All Places > Alfresco Content Services (ECM) > Blog > Author: ddraper
1 2 3 Previous Next

Alfresco Content Services (ECM)

104 Posts authored by: ddraper

Introduction

I've recently been writing some custom clients in order to test out the V1 REST APIs that are available in the 5.2 Alfresco releases in order to provide feedback to the team on areas that can be improved. I've been experimenting with various different UI libraries and frameworks including Vue.js, Aurelia and Angular and have most recently been working with React. When developing anything I always try to follow good coding practices and as such have tried to make the various components that I've built as reusable as possible and have stumbled upon an interesting approach (described in this post) to application composition.

 

Pre-Requisites

The project is ECM only at the moment so you just need a 5.2 Alfresco Repository (there is no need for enabling CORS, you just need to have the repository started and running locally on port 8080). You'll also need to have Node.js (at least version 6) and NPM (at least version 3) installed. You need to use Chrome (it's only a proof-of-concept so I've not yet included some polyfills that are required for other browsers).

 

Setup

Getting everything up and running should only take a couple of minutes. You just need to follow these simple steps:

  1. Download or clone the GitHub repository
  2. Open a terminal in the project directory
  3. Run "npm install"
  4. Run "npm start"
  5. A new browser tab or window should open at http://localhost:3000 showing the following login page.

A screenshot showing the login page

 

The Client

You should be able to gain access to the application by entering valid user credentials for the Alfresco Repository. I'm constantly adding new pages to test out as many of the REST APIs as possible but at the time of writing you should find pages (or "screens" - it is a single page application) for:

 

  • node browsing (table view with drag-and-drop upload)
  • node browsing (film strip view)
  • node details page (accessed by double-clicking on a file name in the table view of nodes)
  • user browsing
  • site browsing
  • tag browsing

 

A screenshot showing the node browser with table view

 

I've intentionally not spent time polishing the design of the client. It is using Material Design Lite to give some basic styling but for the most part I've not done much to any additional styling (for example the upload dialog just shows an overall percentage as a number rather than showing the progress of each individual upload as a progress bar). 

 

The Code

Import the project into your IDE of choice (my preference is Sublime Text 3) and if possible try to ensure that it has appropriate syntax highlighting for JSX (I've used the Babel Sublime plugin for example).

 

The entry point to the client is src/index.js. This file defines the "routes" for the client:

render((
   <Router history={browserHistory}>
      <Route path="login" component={Login} />
      <Route path="/" component={MainLayout} onEnter={requireAuth}>
         <IndexRoute component={Nodes} onEnter={requireAuth} />
         <Route path="nodes" component={Nodes} onEnter={requireAuth} />
         <Route path="users" component={Users} onEnter={requireAuth} />
         <Route path="filmstrip" component={FilmStrip} onEnter={requireAuth} />
         <Route path="sites" component={Sites} onEnter={requireAuth} />
         <Route path="tags" component={Tags} onEnter={requireAuth} />
         <Route path="node/:id" component={NodeDetails} onEnter={requireAuth} />
      </Route>
   </Router>
), document.getElementById('root'))

 

If you wish to add your own route into the client then this is where you should do it. Each route is a React component and you can find the existing route components in the src/routes folder. Create a new file (i.e. "MyRoute.js") for your own route in this folder and then import it in the index.js file:

import MyRoute from "./routes/MyRoute";

...you can then add it as a new client route by inserting it as a new child of the MainLayout route

<Route path="myroute" component={MyRoute} onEnter={requireAuth} />

(note the use of requireAuth to ensure that only logged in users can access the route).

 

Finally you'll want to add a link to your new page into the navigation drawer. This can be done by editing the src/routes/MainLayout.js file that contains the component that defines the layout for the application:

<Drawer title="Links">
   <a className="mdl-navigation__link" href="/nodes">Nodes</a>
   <a className="mdl-navigation__link" href="/users">Users</a>
   <a className="mdl-navigation__link" href="/filmstrip">Filmstrip</a>
   <a className="mdl-navigation__link" href="/sites">Sites</a>
   <a className="mdl-navigation__link" href="/tags">Tags</a>
   <a className="mdl-navigation__link" href="/myroute">My route</a>
</Drawer>

Now add the source code into MyRoute.js to define the component for your new route:

import React from "react";
import Content from "../components/layouts/Content";

const MyRoute = React.createClass({
   render() {
      return (
         <Content>Hello World</Content>
      );
   }
})

export default MyRoute;

Each time you save a file the application should hot-reload so you should see the new link in the drawer:

Screenshot showing the new link in the drawer

...and when you click the link it will take you to the new route..

Screenshot showing the new route

 

Composing Your Route

The render function in the MyRoute component is one of the React Component lifecycle functions and returns JSX. The great thing about this is that it is possible to mix-and-max JavaScript, HTML and other React Components. This allows you to compose the content of your route in much the same way as you'd write a standard HTML page. 

 

The components that are currently available in the repository have been written to be as re-usable and as flexible as possible. The components can be divided into container and presentation components where the container components are responsible for retrieving and setting data on the Alfresco Repository (via the V1 REST APIs) and setting state that is pushed down through the nested presentation components. 

 

Whilst state is pushed down to presentation components I've taken the approach to use custom native DOM events to bubble actions back up from those presentation components to the container components. This is a slightly different approach than the suggested approach of passing callbacks as properties but does avoid the problem of "drilling holes" to reach deeply nested components.

 

In practice this means that a presentation component can always communicate with its containing component no matter how deeply it is nested. It also means that the components in between the container and the deeply nested presentation component do not need to be involved or have any awareness of that communication.

 

The Collection Component

The starting point of many a client is in showing a collection of something... nodes, users, sites, search results, etc. The great thing about the new REST APIs is that they provide a completely consistent way of requesting and receiving data. This means that the same collection component (components/containers/Collection) can be used for any GET REST API that returns a list of items. It also means that you can use the same pagination controls component (components/controls/Pagination) to control the page size and what page of available data is displayed. Common events are defined and exported from the components/containers/Collection.js file that can be emitted by any component nested within a collection to handle:

 

  • new item creation
  • existing item updates
  • reordering of data (field and/or direction)
  • filtering
  • navigation and relative path setting
  • updating page size
  • changing the displayed page

 

So whether you want to show nodes, users, tags, favourites, sites, whatever you can use the same component.

 

Single Responsibility Principle

The collection component follows the single responsibility principle in that it is only responsible for retrieving data and it does not deal with how that data is displayed. Instead it handles off that responsibility to other components that can be nested anywhere inside it. This means that the view of the data is completely customizable.

 

At the time of writing I've provided 3 different data views:

 

  • A table view (components/views/TableView) that relies on nested components to define the structure of that table
  • A table view of user data (components/views/UserTableView) that is a component composed of a mixture of HTML and other components
  • A carousel view (components/views/Carousel) that relies on nested components to define how each item in the carousel is displayed.

 

...and you can of course both provide entirely new views or nest existing or custom components within those view components to construct any view that you want with complete freedom.  For example compare this example in the sites route:

<Collection url="/api/-default-/public/alfresco/versions/1/sites" relations="containers,members">
   <TableView>

      <TableViewHead>
         <TableHeading label="ID" />
         <TableHeading label="Name" />
         <TableHeading label="Actions" />
      </TableViewHead>

      <TableViewBody>
         <TableCell property="id"/>
         <TableCell property="title"/>
         <TableCell>
            <Delete url="/api/-default-/public/alfresco/versions/1/sites"></Delete>
         </TableCell>

      </TableViewBody>
     
      <TableViewFoot>
         <TableCell colspan="3" >
            <Pagination />
         </TableCell>
      </TableViewFoot>

   </TableView>
</Collection>

 

...with this on in the filmstrip route:

<Collection url="/api/-default-/public/alfresco/versions/1/nodes/-root-/children" 
            orderBy="name"
            include="properties">

     
   <BreadcrumbTrail/>

   <Carousel frameHeight="300px">
      <Property property="name" navigation={true}></Property>
      <Thumbnail renditionId="imgpreview"></Thumbnail>
   </Carousel>
  
</Collection>

 

Note how the same collection component is used (configured with different URLs to access different data) but completely different components are used to compose how the data is displayed.

 

Custom Rendering and Extension

It's also worth noting in the sites example how a TableCell can easily be used to either render a property of an item or it can simply nest another component like the Delete component to render something completely different (in this case a delete action that prompts for confirmation). It's also worth noting that the exact same Delete component can be used for deleting any item from any collection because of the consistent way in which the REST APIs now handle DELETE operations.

 

In the Carousel example you'll see that the name of the node is displayed using the Property component. This component could just as easily have been nested inside a TableCell and in fact both components extend the abstract Render component. This is just one example of how it is possible to reuse existing capabilities by extending an existing component. The means that when it is not possible to render data as you'd prefer you can simply create your own component (and optionally choose to extend Renderer if appropriate) and then nest it anywhere within your view.

 

Hierarchical Freedom

The components are written in such a way that you are completely free to compose them as you wish. For example in the routes/Nodes component you'll see that the TableView is nested within an UploadTarget component. The UploadTarget component provides a target area for dragging-and-dropping files from the operating system to upload into the Alfresco Repository. The key thing here is that it does not need to be nested within a Collection and the Collection does not need to nest it.

 

You could just add an UploadTarget anywhere on the page so that files dragged-and-dropped onto any of the nested components will be uploaded to the configured location in the Alfresco Repository (you should note that for convenience the url property will be inherited from a parent unless specifically provided). 

 

This freedom also means that it's possible to include BreadcrumbTrail components and Toolbar components into the Collection without any problems - but the benefit is that they can influence the Collection (for example to change the relative path or refresh the list when a new folder is created) by simply emitting on of the registered custom events.

 

Documentation

I've made an attempt to provide documentation for some of the components using JSDoc. You can build this documentation locally by running:

npm run generate-docs

...and you can then browse the documentation by opening the docs/index.html file in a browser. 

Deploying React pages to Share

Posted by ddraper Jan 18, 2017

Introduction

I've previously experimented with developing Alfresco clients using a number of different application frameworks. In this blog post I'm going to step through the process of creating a page using React and deploying it into Share.

Although I'm using React in this example I could have just as easily used Vue.js, Aurelia, Angular, Ember or any framework that provides a CLI or project template for developing and packaging a Single Page Application.

Basic Setup

  1. Install the create-react-app CLI following the instructions on the linked page
  2. Create a new application:
    create-react-app MyApp
  3. Change to the project directory:
    cd MyApp
  4. Eject!
    npm run eject
  5. Edit the "scripts/start.js" file and add the following at the end of the "addMiddleware" function:
    let alfrescoProxy = httpProxyMiddleware('/share/proxy/alfresco-api', {
       target: 'http://localhost:8080/alfresco',
       changeOrigin: true,
       pathRewrite: {
          '^/share/proxy/alfresco-api': 'api'
       }
    });
    devServer.use(alfrescoProxy);
  6. Install alfresco-js-utils (the version used at the time of writing is specified, newer versions maybe available at time of reading!):
    npm install alfresco-js-utils@0.0.7 --save-dev
  7. Start the application:
    npm start

 

Why Eject?

Strictly speaking it is not necessary to eject (and it would be preferable not to). However, in this example I'm re-using my own NPM package called alfresco-js-utils which provides some re-usable JavaScript functions. One of these functions is a service for retrieving Nodes from the Alfresco Repository using a V1 REST API and the URL that this function calls starts with /share/proxy/alfresco as it is intended to ultimately be run on Share. It is possible to configure an API proxy for the create-react-app CLI but it does not support URL rewriting. Therefore we need to eject in order to gain access to the start.js script that is edited to allow us to add custom HTTP proxy middleware.

 

Create Your Page

You should now have an application running locally with a great development experience with hot-reloading, etc provided by create-react-app. If you make an REST API call to (using an URL starting with /share/proxy/alfresco) then you should get a basic authentication challenge and once you've provided some valid credentials you'll be able to access data from the Alfresco Repository (don't worry, when you deploy into Share the authentication will be handled for you by Share/Surf).

 

I've built a very simple client for browsing company home. You can view/copy the code from this GitHub repository (check out this tag for the code at the time of writing this post). The client contains 4 components:

  • List
  • ListView
  • Breadcrumb
  • Toolbar

...that provide a simple interface for browsing.

 

Build and Deploy

Once you're happy with your page then run:

npm run build

This will populate the build folder with the resources that you want to deploy into Share. 

 

  1. Copy the build folder to a new location
  2. Rename the static folder to be "META-INF"
  3. Create the following files:
    1. alfresco/site-data/pages/react.xml
    2. alfresco/site-data/template-instances/react.xml
    3. alfresco/templates/react.ftl

 

Your folder should now look like this:

 

Now update the files as follows:

 

alfresco/site-data/pages/react.xml

<?xml version='1.0' encoding='UTF-8'?>
<page>
   <title>React</title>
   <description>React</description>
   <template-instance>react</template-instance>
   <authentication>user</authentication>
</page>

This defines the new Surf page that will contain our React code.

 

alfresco/site-data/template-instances/react.xml

<?xml version='1.0' encoding='UTF-8'?>
<template-instance>
   <template-type>react</template-type>
</template-instance>

This is referenced from the page and creates a mapping to a template.

 

Copy the contents of the index.html file into the alfresco/templates/react.ftl file but replace all occurrences of "/static" with "/share/res".

If you change the contents of any JS or CSS and rebuild it will be necessary to copy and update the contents of index.html again because the resources are named with a checksum (just like Surf/Aikau does!)

Now bundle up the contents of the copied build folder (not including the build folder itself) as a JAR file and copy it into the WEB-INF/lib directory of Share and restart the server that it is running on.

 

Once the server has restarted you'll be able to login to Share and then you will be able to access your page at /share/page/react.

 

 

Include the Header and Footer

If you want your users to access this new page with the context of Share then it makes sense to include the standard header and footer.

 

Update the react.ftl file initially so that it looks like this:

<#include "/org/alfresco/include/alfresco-template.ftl" />
<@templateHeader>
   <!-- Insert CSS link here -->
</@>
<@templateBody>
   <div id="alf-hd">
      <@region scope="global" id="share-header" chromeless="true"/>
   </div>
   <div id="bd">
      <!-- Insert contents of body element here -->
   </div>
</@>
<@templateFooter>
   <div id="alf-ft">
      <@region id="footer" scope="global" />
   </div>
</@>

 

...and then add the CSS <link> element and the contents of the <body> element from index.html into the commented sections.

 

Update your JAR file with the changes, redeploy and restart the server and when you access the same URL you'll see the header and footer.

 

Add a Link to the Header

Obviously it's not ideal for users to have to enter a URL into the browser to get to your new page so let's add a link into the header bar.

 

This Stack Overflow question and answer provides a good overview on doing this properly (go on, give it an up vote - you know you want to!) but the basic steps in this case are as follows:

 

  1. Create an alfresco/site-data/extensions/react-extension.xml file containing:
    <extension>
       <modules>
          <module>
             <id>React Extension</id>
             <auto-deploy>true</auto-deploy>
             <customizations>
                <customization>
                   <targetPackageRoot>org.alfresco</targetPackageRoot>
                   <sourcePackageRoot>org.alfresco.share.pages.customizations</sourcePackageRoot>
                </customization>

                <customization>
                   <targetPackageRoot>org.alfresco.share.pages</targetPackageRoot>
                   <sourcePackageRoot>org.alfresco.share.pages.customizations.share.header</sourcePackageRoot>
                   <alwaysApply>
                      <webscript>share-header</webscript>
                   </alwaysApply>
                </customization>
             </customizations>
          </module>
       </modules>
    </extension>
  2. Create an alfresco/site-webscripts/org/alfresco/share/pages/customizations/share/header/share-header.get.js file containing:
    var headerMenuBar = widgetUtils.findObject(model.jsonModel.widgets, "id", "HEADER_APP_MENU_BAR");
    if (headerMenuBar && headerMenuBar.config && headerMenuBar.config.widgets)
    {
       headerMenuBar.config.widgets.push({
          name: "alfresco/menus/AlfMenuBarItem",
          config: {
             label: "React page",
             targetUrl: "react"
          }
       });
    }
  3. Repack your JAR file, deploy it to /share/WEB-INF/lib and restart the server

 

Now on each page in Share you'll see a link to your new page:

 

Summary

It's very easy to take advantage of the development environments provided for modern web application frameworks and deploy the output into Share. Some of the techniques shown in this post may be old but are still extremely effective which shows the value in a properly architected framework.

Deja-Vue

Posted by ddraper Jan 16, 2017

Introduction

If you've been following my personal blogs then you'll know that I've been experimenting with some of the currently popular web development frameworks (Vue.js, React, Aurelia and Angular). Each comes with their own advantages and disadvantages but my personal preference (and everybody is entitled to their own preference!) was for Vue.js.

 

There are lots of reasons to like Vue.js (many of which are summarized described here) but one of the things that I found really useful was that you could very easily write it in plain ol' ES5 JavaScript.

 

An Argument Against Transpilation

Now don't get me wrong... I love writing in ES6 and I get positively giddy when writing in TypeScript (although that might just be down to my flagrant abuse of the "any" keyword) but ultimately the code you write in ES6 and TypeScript is going to get transpiled down into ES5 and that can mean that the code ends up both larger and in some cases actually less efficient which will actually result in poor performance.

 

Bundling and Tree Shaking

The other interesting thing about these new frameworks is that they all offer lovely development environments with hot-reloading which can really enhance development speed.... however, when you come to deploy your code you still need to bundle it up and will want to tree-shake and minify (or uglify) the code - and you have to do this each time you want to deployment your code.

 

The idea of tree-shaking is that you remove all the code that isn't required on the page in order that the page load is faster to boost performance. Surf has actually been providing this capability via Aikau since March 2013. The fundamental differences being that it is done in Java (and not via Node.js) and that it does this efficiently at production time through aggressive caching. 

 

  • Yes, you can load System.js into the browser and transpile and asynchronously load from the client but the performance will be poor.
  • Yes, it is true that that there is some initial overhead to Surf/Aikau performing dependency analysis on the first page load this then disappears for all subsequent page loads through aggressive caching.
  • Yes, it's also true that Node.js will do the necessary string processing required for dependency analysis must faster than Java, there isn't (to my knowledge at least) any Node.js middleware that is offering this capability.

 

Re-usability

The other aspect of web application development that has always interested me is in re-use. It's really easy to write some JavaScript component that is re-usable as a "leaf node" and you can probably provide some configuration options to customize its appearance or behaviour.

 

We should all be implementing components following the "single responsibility principle" that will typically result in a nested component hierarchy. This is perfectly fine where you own that entire hierarchy because you control all the components and are the only one using them. But what if you want to share your components for others to use? And what if they want to customize those components?

 

Let's say you provide component A which contains component B which contains component C. Someone else wants to replace C with D, so they have to customize B to create D instead of C (which results in component E) and then customize component A to instantiate E instead of B. So the end goal was A contains B contains D, but the result was F contains E contains D.

 

That's 3 extra components that you now have create and maintain instead of 1.

 

Transclusion, et al

Some of the frameworks provide a solution to this that is variously called "transclusion" (Angular), "content projection" (Aurelia) and "content distribution" (Vue.js) but I've found that these only really work to a single level of nesting and aren't the easiest things to implement. 

 

The reason that this is a hard problem boils down to dependency management. A component will need to declare the sub-components that it can contain in order to be able to render them.

 

Aikau has always provided a solution to this problem by allowing developers to compose pages of widgets in a JSON model that is analysed by Surf to establish all of the dependencies prior to rendering. Widget references in the model can easily be swapped out, removed or reconfigured without needing to update any other widgets in the model.

 

Unfortunately one of the main criticisms levelled at Aikau was that it's hard to find people with the skills despite the fact that all you really need is some basic web development skills and an understanding of 3 very simple concepts (as described here).

 

So this got me to thinking...

 

An Idea...

We already have a great tree-shaking and module loading solution in Surf (albeit one that is desperately uncool) that provides an excellent basis for providing re-usable components (as ably demonstrated by Aikau for over 100 releases).... what if we could marry that existing infrastructure with some cool new JS framework?

 

So that's what I've done.

 

I've added a couple of widgets into Aikau that allow you to easily build Vue.js components that can be composed together in the traditional Aikau page models. This means that if you can get your head around some trivial boilerplate code then you can create re-usable and Vue.js components.

 

The Solution...

So the boiler plate code looks like this:

define(["dojo/_base/declare",
        "aikau/vue/Base",
        function(declare, Base) {
  
   return declare([Base], {

      getComponentElement: function() {
         return "";
      },

      getComponent: function getComponent() {
         return {
         };
      }
   });
});

 

There are two functions that need to be implemented:

  • getComponentElement: returns a string with the element custom component element name
  • getComponent: returns a Vue.js component object

 

For example, I implemented the same simple application as I had previously done in standalone Vue.js (and React, Aurelia and Angular!) which resulted in 4 components including a toolbar component containing forward and back pagination buttons that looks like this: 

define(["dojo/_base/declare",
        "aikau/vue/Base",
        "dojo/text!./templates/Toolbar.html"],
        function(declare, Base, template) {
  
   return declare([Base], {

      getComponentElement: function() {
         return "toolbar";
      },

      getComponent: function aikau_vue_Toolbar__getComponent() {
         return {

            template: template,

            props: ["list"],

            methods: {
               back: function() {
                  var changeEvent = new CustomEvent("pageBack", {
                     bubbles: true
                  });
                  this.$el.dispatchEvent(changeEvent);
               },

               forward: function() {
                  var changeEvent = new CustomEvent("pageForward", {
                     bubbles: true
                  });
                  this.$el.dispatchEvent(changeEvent);
               }
            }
         };
      }
   });
});

 

These components can be composed in an Aikau page model like this:

model.jsonModel = {
   widgets: [
      {
         name: "aikau/vue/Bootstrap",
         config: {
            widgets: [
               {
                  name: "aikau/vue/List",
                  config: {
                     widgets: [
                        {
                           name: "aikau/vue/Breadcrumb",
                           config: {
                              "v-bind:relativePath": "relativePath"
                           }
                        },
                        {
                           name: "aikau/vue/Toolbar",
                           config: {
                              "v-bind:list": "list"
                           }
                        },
                        {
                           name: "aikau/vue/ListView",
                           config: {
                              "v-bind:list": "list"
                           }
                        }
                     ]
                  }
               }
            ]
         }
      }
   ]
};

 

How it Works

The solution is pretty straightforward. The "aikau/vue/Base" module (that all the Vue.js component widgets must extend) iterates over any child widgets and registers a local component for them. The child component element (the value returned from getComponentElement) is written into the template and the properties defined in the widget config are output as attributes of that component.

 

If you're familiar with Vue.js then you'll note the use of the "v-bind:" prefix that is used in this case to create a dynamic binding between the "list" value in the List widget and the Toolbar and ListView widgets. This means that when "list" is updated in the List component the Toolbar and ListView components can reactively update to reflect the changes.

 

The List component template contains a single property "${widgets_slot}" that is swapped out with the locally registered child components, so that...

<div @navigate="navigate" 
     @setRelativePath="setRelativePath"
     @pageBack="pageBack"
     @pageForward="pageForward">

   ${widgets_slot}
</div>

...becomes...

<div @navigate="navigate" 
     @setRelativePath="setRelativePath"
     @pageBack="pageBack"
     @pageForward="pageForward">

   <breadcrumb :relativePath="relativePath"></breadcrumb>
   <toolbar :list="list"></toolbar>
   <list-view :list="list"></list-view>
</div>

...when the child components are rendered.

 

Limitations

The only limitation that I've found with this approach is that I wasn't able to take advantage of the Vue.js event model and needed to fall back to native custom events, but the only downside I could see to this was that you wouldn't be able to benefit from the excellent debugging tools provided by the Vue.s developer extension for Chrome.

 

Why?

So if you're creating a Vue.js application (or indeed an application using any other frameworks) then you probably won't want to jump through these hoops. But if you want to provide an application that you want people to be able to customize (like Alfresco Share) then you need to allow developers to make small (or large) changes to deeply nested components without copy/pasting code or extending every component in the hierarchy of the only one they actually need to change.

 

Even in this very simple 4 component example it means that you could swap out the ListView component without needing to change the parent component. The ListView component itself could be re-written to be composed from a library of metadata renderering components that could be selected from to render views specific to the data displayed.

 

This approach also allows composition of components that haven't been written with respect to each other - once again something that we have been able to successfully benefit from using Aikau at Alfresco.

 

What Next?

I strongly doubt that this implementation is going to be used anywhere unless there is any kind of major demand for it from customers, our partners or the community. It currently exists on an unmerged branch of Aikau but could easily be integrated into the main code base and released for use.

 

Could This Approach be Used with Framework {X}?

The main reason why I picked Vue.js for this experiment takes us back to the original point way back at the beginning of this post. Vue.js can very happily be run without transpilation in ES5 code. This same approach could theoretically be applied to React but to get the best out of it you'd want to use JSX and that would require a transpilation step that Surf currently doesn't provide. 

 

It would obviously be possible to transpile the component code as part of the build process for Aikau but that would introduce its own set of problems as Surf/Aikau doesn't provide source maps which would create a barrier to effective debugging.

 

Conclusion

If you've reached the end of this very long post then thanks for your perseverance! I'd really appreciate any comments and thoughts that you might have on this - especially on whether you think the use case being addressed is even a valid one.

Improving Aikau Performance

Posted by ddraper Dec 12, 2016

When I presented a Tech Talk Live session on Aikau a few weeks ago I mentioned that I would be looking at performance issues. We'd already had some excellent analysis done by Axel Faust and I had done some initial prototyping but I'd not had the opportunity to properly focus on it. The discussion also came up in the IRC channel so I figured that it was about time that I looked into it properly.

 

I set up a performance test page that consisted of a list with a single view containing a table of 100 rows, each with 50 cells containing a single Property (so over 10,000 widget instances) where each Property was assigned 50 custom properties. This is not typical for an Aikau page but was the scenario that Axel Faust had previously encountered and was designed to help flush out where performance issues were originating.

 

This test page took 55 seconds to load in debug mode (PLEASE NOTE: This is not representative of normal page rendering - a typical existing Aikau page in Share on the same system takes under 2 seconds to render in debug mode).

 

Chart showing worst performance timeline

 

I spent a few days working through the troublesome areas and figuring out ways to drastically improve performance and have reached the point where once all the changes are integrated the performance test page load was reduced to under 15 seconds (a 366% improvement!) and under 10 seconds in production mode.

 

Improved Performance

In a more realistic test (with 25 rows and 5 columns) the rendering went from around 2 seconds to under 1 (but still a 100% improvement):

 

More reprentative Test before performance enhancmentsMore reprentative Test after performance enhancments

 

The biggest challenge was is to be able to fold all these improvements back into the main code base in such a way that:

 

  • All widgets work exactly as they did before
  • Function signatures in modules don't change
  • Execution code path doesn't change

 

It is the last 2 issues that are the most technically challenging but are important because we don't want to break any 3rd party extensions to existing widgets - and ensuring backwards compatibility of Aikau releases is our top priority!

 

The main changes that we've made can be summarized as follows:

 

  1. Building widget elements outside of the main document body (to avoid unnecessary browser rendering cycles)
  2. Building each widget DOM natively (rather than using the capabilities provided by dijit/_TemplatedMixin
  3. Bypassing unnecessary property handling in dijit/_WidgetBase
  4. Switching from using dojo/Deferred to native Promises

 

There is a new BaseWidget module that new widgets should use (and existing widgets will be converted to use) and a new ChildProcessing mix-in module that provides the function "createChildren" to replace "processWidgets". 

 

We made some initial improvements in the 1.0.100 release but the main improvements will be in the 1.0.101 release. At the moment that changes are confined to the list rendering and I will be working through the remaining renderer widgets to update them to build their DOM natively. The next stage will be to convert the remaining widgets to take advantage of the new code.

 

There's still a way to go before all the whole Aikau codebase is updated to take advantage of these improvements but I thought it would be useful to let people know that it was something we were focusing on. It also further shows how we're able to make these sorts of improvements in Aikau without impacting any existing pages.

Introduction

After I wrote my last blog post on customizing the Create and Edit Site dialogs I realised that there was no single point of consolidated information on all of the many capabilities that are available in Aikau forms. Rather than trying to write everything up in a blog post I've decided to record a series of videos demonstrating as many of the different features as I could. The videos (that I've completed so far!) are embedded in this post but are available separately within the Alfresco Community platform. I've tried to break the content into manageable chunks (and still have a couple more videos to record) but there is still a lot of footage to get through!

 

Part 1 - The Basics

This video goes through the basics of setting up a form and the common configuration attributes that all form controls share.

 

Part 2 - Advanced Data Handling

This video goes through options for setting values in the form (via publication, URL hash, etc) as well as covering form warning configuration.

 

Part 3 - Options Handling

This video covers dynamic options handling showing how to access data from the Alfresco Repository in a variety of different form controls as well as examples for configuring how multi-value data is handled.

 

Part 4 - Layout and Dialogs

Coming soon!

 

Part 5 - Multiple Entry Form Controls

Coming soon!

Introduction

If you've tried out the Early Access Community Release (201611) of Alfresco you might have noticed that in version 5.2 the Create and Edit site dialogs are now being rendered by Aikau. As well as providing some user experience improvements the main benefit of switching to Aikau is to make it much easier to customize both dialogs. In this post I'm going to provide some examples of the options that are now available to you.

PLEASE NOTE: The early access release doesn't have all of the customization options described here - to try them all out you'll need version 1.0.98 of Aikau.

 

Don't Panic

If you have already made significant customizations to the the YUI2 based versions of the dialog then don't panic. We've provided a very simple way in which to use the old dialogs so that your existing customizations can continue to work just as they've always done. The SiteService that controls which dialogs are used has a legacyMode configuration attribute that when set to true will use the YUI2 dialogs. In Share 5.2 we are re-configuring this to be false so if you want to continue using the YUI2 dialogs you just need to create an extension that configures it back to true again!

 

Download Extension

The first job is to actually build an extension module to customize the SiteService on all pages within Share.... however, you may not want to do that so I've provided an extension module that does exactly this in this GitHub repository. You can download a pre-built extension from here (to re-enable the YUI dialogs) or use it as a starting point for implementing the other customizations described in this post (but don't forget to switch legacyMode to false!!!)

 

Custom Presets

Alfresco only comes with a single site preset (the "Collaboration Site"). In order to make more site presets show up in the old dialog it was necessary to create an extension module to customize the create-site.get.js WebScript JavaScript controller. All customization is now done via the SiteService and you now have options to:

 

  • Explicitly define the presets (via the sitePresets attribute)
  • Provide additional presets (via the additionalSitePresets attribute)
  • Remove existing presets (via the sitePresetsToRemove attribute)

 

Let's add some additional presets. In the share-header.get.js customization file create the following:

 

var siteService = widgetUtils.findObject(model.jsonModel, "id", "SITE_SERVICE");
if (siteService && siteService.config)
{
  siteService.config.additionalSitePresets = [{ label: "Custom Preset", value: "CUSTOM" } ];
}

 

This will add the additional preset into the create site dialog, like so:

 

Customized create site dialog with additional preset

 

Dialog Model Overrides

The widgets model for the create site dialog is defined in the widgetsForCreateSiteDialog attribute. You can completely replace the model if you wish - but a more efficient way to make changes would be to make use of the widgetsForCreateSiteDialogOverrides attribute. This is an additional model that lets you...

 

  • add widgets (at the start and end or before/after a specific widget in the default model)
  • remove widgets
  • replace widgets
  • merge additional configuration into existing widgets

 

Removing Fields

Let's start by removing the "Description" field. Update the share-header.get.js file so that it looks like this:

 

var siteService = widgetUtils.findObject(model.jsonModel, "id", "SITE_SERVICE");
if (siteService && siteService.config)
{
  siteService.config.additionalSitePresets = [{ label: "Custom Preset", value: "CUSTOM" } ];
  siteService.config.widgetsForCreateSiteDialogOverrides = [
    {
      id: "CREATE_SITE_FIELD_DESCRIPTION",
      remove: true
    }
  ];
}

 

This instructs the SiteService to remove any widget with the id attribute "CREATE_SITE_FIELD_DESCRIPTION" and has the effect of removing the "Description" field as shown here:

 

Create site dialog with the description field removed

 

Updating Fields

There are two ways in which you can update an existing field. The first method is using the replace attribute. As the name suggests this will completely replace any widget with a matching id. Add the following entry into the widgetsForCreateSiteDialogOverrides array:

 

{
  id: "CREATE_SITE_FIELD_VISIBILITY",
  replace: true,
  name: "alfresco/forms/controls/Select",
  config: {
    fieldId: "VISIBILITY",
    label: "How am I seen?",
    name: "visibility",
    optionsConfig: {
      fixed: [
        {
          label: "By everyone",
          value: "PUBLIC"
        },
        {
          label: "By Some",
          value: "MODERATED"
        }
      ]
    }
  }
}

 

...and the result is that the original RadioButtons widget is replaced by a Select widget like this:

 

Create site dialog with a replaced field

 

If you just want to merge some existing configuration into an existing field then you can just omit the replace attribute and just provide the new or changed attributes. Change the replace entry to be this:

 

{
  id: "CREATE_SITE_FIELD_VISIBILITY",
  config: {
    label: "How am I seen?",
  }
}

 

This has the effect of simply updating the label displayed:

 

Create site dialog with an updated field

 

Adding New Fields

If you just want to add a brand new field into the dialog then you can do so using the targetPosition attribute. This accepts the following 4 different values:

 

  • "START"
  • "END"
  • "BEFORE"
  • "AFTER"

 

When using "BEFORE" or "AFTER" it is also necessary to specific a targetId attribute to indicate which existing field the new fields should be added in relation to.

 

For example, let's say that you want to add a new field at the start of the dialog. Add the following entry into the widgetsForCreateSiteDialogOverrides array:

 

{
  id: "FIRST",
  name: "alfresco/forms/controls/TextBox",
  targetPosition: "START",
  config: {
    fieldId: "FIRST",
    label: "First!",
    name: "first",
    description: "This field has been added as the first entry"
  }
}

 

This will result in the following dialog:

 

Create site dialog with a new field at the start of the dialog

 

If you wanted to insert a field between the "Site ID" and "Name" fields then use a combination of targetPostition and targetId like this:

 

{
  id: "INSERTED",
  name: "alfresco/forms/controls/TextBox",
  targetPosition: "BEFORE",
  targetId: "CREATE_SITE_FIELD_SHORTNAME",
  config: {
    fieldId: "INSERTED",
    label: "Inserted",
    name: "inserted",
    description: "This field has been inserted before another field"
  }
}

 

This would then result in the following dialog:

 

Create site dialog with a new field inserted in the middle of the form

 

How the Form Works

The examples shown are very simplistic and are provided to demonstrate the principals of customizing the dialog forms. You can make full use of any and all of the capabilities provided by the many Aikau form controls that are available. You can find a full list of the form controls available in the JSDoc (look under the alfresco/forms/controls package) and can find examples of the basic form concepts in a Sandpit page.  Lots of information about form control configuration can also be found in videos embedded in this blog post.

 

Any form control added to the dialog will automatically have it's value included in the XHR request to the /modules/create-site and /modules/edit-site WebScripts respectively. It will therefore be necessary to also customize or override these WebScripts to make use of the extra data that your customized dialogs provide (for example you may wish to apply custom Aspects to the site and set additional metadata on them). However, these changes are outside the scope of this article which is focused solely on the client-side user interface changes.

 

Reusable Mix-in Module

The overriding capabilities are provided through the new module WidgetsOverrideMixin introduced in Aikau 1.0.97. This module can be mixed into other widgets and services to provide the same model manipulation. If there are any existing areas of Aikau code that you feel could benefit from this capability then please let me know!

 

Missing Use Cases?

It's quite possible that these capabilities still do not meet your specific customization requirements for the create and edit site dialogs. I did create a poll a few weeks ago to try and gather as much information as possible but didn't get many concrete use cases to address. If you feel that these updates don't support your specific requirements then please let me know in order that we can address them. 

Introduction

With the 1.0.96 release of Aikau we've introduced a new AMD package (simply called "aikau") that will give you a sneak peak of where things are going to be heading. This post is going to explain why we've made this addition, what it will do for you and how you can contribute to its success.

 

Future Directions

If you watched my recent Tech Talk Live presentation you'll have a good idea about the history of Aikau and you'll also have been given a brief overview of where we want to take it. The first couple of items on the road map were performance improvements and the introduction of Material Design styled widgets. Alfresco has decided to adopt Material Design and this means that at some point in the future we may want to update Share to use this "visual language" (Google's terminology, not mine). 

 

The simplest approach to achieving this aim is to convert more of Share to use Aikau, and then either update the styling of the widgets used to define the pages to use Material Design or swap them out with dedicated Material Designed widgets. 

 

At the same time we also want to apply the lessons that have been learned over the last 4 years to this new suite of widgets. One of our most valuable Community members (Axel Faust) provided us with some great analysis of how Aikau performs and we know that there is room for improvement. 

 

Right and Wrong

Some things we got right... Fine grained components that follow the "Single Responsibility Principle" (having learned our lesson from the coarse grained YUI2 components) being one and another being decoupling over a publication / subscription model (whilst adding complexity) to provide clean customization, easy enhancement and promote reuse. I did however initially imagine that a widget would only ever have one set of child widgets... and this proved to be incorrect and I also didn't focus enough on the performance of generating child widget at large scale. 

 

New Package, New Rules

Aikau is released on a weekly basis and every release is guaranteed to be backwards compatible with the last. We also try to avoid removing functions or changing their signatures to ensure that 3rd party widgets that rely on them don't break. This makes working on something as complex as performance quite an interesting challenge.

 

Therefore we've introduced a new package called "aikau" (the original package is called "alfresco") in which we're going to create our new Material Design style widgets that inherit from from new, performance enhanced modules. In order to make it easier to collaborate with the Alfresco Community the new "aikau" package will not be subject to the same stringent, backwards compatibility rules as the original "alfresco" package.

 

This means that we will be able to make breaking changes between releases should it be necessary and therefore you shouldn't start using these in production yet. Ultimately we want the "aikau" package to be a safe collaboration space that will eventually form a 1.1 release of Aikau. At which point the original "alfresco" package will be deprecated (but not removed) as there will be a full complement of Material Design widgets and production applications (such as Share) can begin to transition to using them. 

 

What This Means

This gives us the best of both worlds - the "alfresco" package will continue to be developed, new widgets will be added, bugs will be fixed and weekly releases will continue... but we will open up the contribution process of the "aikau" package to encourage more active participation without needing to worry about backwards compatibility or regression testing until we're confident that we've got the implementation right. 

 

If you have an interest in the future direction of Aikau and Share then now is the time to get involved. In my next post I'll showcase the new Material Design widgets and discuss what we've done so far and the other areas where we're looking to improve performance. If you have additional ideas of where you'd like to see Aikau go then we'd love to hear about them.

 

Extending Aikau Widgets

Posted by ddraper Nov 1, 2016

Introduction

I'm very keen to understand the pain points of using Aikau for customizing Alfresco Share and for creating standalone clients. I recently got some very valuable feedback from an Alfresco Community member suggesting that one area that I could try to describe better would be in extending existing Aikau widgets. I'm always very want to write posts that help people overcome any issues so I've written this to try and help people understand the various options available to them - the Aikau tutorial provides a chapter on creating new widgets but doesn't have any information on extending them.

 

Custom Styling

The specific use case that was raised was the issue of customizing the CSS used for a widget. Ideally we aim to make it possible to customize the styling of widgets through LESS variables in the Alfresco Share themes. Aikau provides a set of default styling variables that are referenced by the widgets covering attributes such as:

  • fonts (colour, size, family, etc)
  • borders
  • colours
  • spacing (horizontal and vertical margins and padding, line heights, etc)

...and the CSS files associated with a widget will reference these variables. This means that it is possible to customize styling by editing or creating an Alfresco Share theme as shown in this example.

 

An example use of these variables is shown here (taken from AlfBreadcrumb.css) - see the use of @link-font-color, @link-text-decoration and @breadcrumb-background-color - these are all examples of using the LESS variables.

 

.alfresco-documentlibrary-AlfBreadcrumb a {
   color: @link-font-color;
   text-decoration: @link-text-decoration; 
   padding: 5px 15px 5px 30px;
   background: @breadcrumb-background-color;
   position: relative; 
   display: block;
   float: left;
   cursor: pointer;
   height: 20px;
   &:hover {
      colour: @link-font-color-hover;
      text-decoration: @link-text-decoration-hover;
   }
}

 

It is always worth referencing the LESS variables in the CSS files of your custom widgets - and if you find that an Aikau widget is not making use of a variable or you feel that styling customization would be easier if a variable was used then please raise an issue.

 

Extending to Add Custom Styles

If it is not possible to set a LESS variable in your Alfresco Share theme to achieve your desired styling then you can always extend a widget and provide additional CSS files. The Surf framework that Aikau uses to build the resources required for each page will parse the source of each widget to identify the CSS dependencies that need to be included (as described in this blog post). Therefore in order to provide additional dependencies it is necessary to include them in a widget source file.

 

Let's say you want to extend add in a new CSS dependency for the alfresco/menus/AlfMenuItem widget - the perfect example would be the alfresco/headerAlfMenuItem as it does exactly this - the entire module looks like this (with the JSDoc removed)

 

define(["dojo/_base/declare",
        "alfresco/menus/AlfMenuItem"],
        function(declare, AlfMenuItem) {

   return declare([AlfMenuItem], {

      cssRequirements: [
         {cssFile:"./css/AlfMenuItem.css"}
      ]
   });
});

 

The main thing that this widget does is to provide a new CSS dependency that changes the styling of the menu item to be the style that you see in the Share header (i.e. with a black background) this image shows the difference:

 

ExtendingWidgetsBlogPost1.png

The functionality provided by alfresco/menus/AlfMenuItem is retained - all alfresco/header/AlfMenuItem does is to restyle the widget.

 

Extending Widget Functionality

The other major reason for extending is to either add or change the functionality of an existing widget. Aikau strives to not only be backwards compatible itself but to also ensure that 3rd party customizations (including extended widgets) continue to work against new releases. For that reason we will never remove a function from a widget (except when moving to a new major version and only then if the function has been previously deprecated - you can find a list of all deprecations in the release notes).

 

As well as this we use special annotations in the widgets to indicate which functions can be overridden (that is be completely replaced) or extended (be replaced but with a call to this.inherited(arguments) to call the inherited behaviour). Functions that can be overridden will be marked with @overrideable and those than can be extended will be marked with @extendable and these annotation will show up in the JSDoc as a "Support" statement as shown:

 

ExtendingWidgetsBlogPost2.png

 

As a general rule we don't automatically add these annotations to all functions - but if you have a clear requirement for a function to be supported in future releases then you should raise and issue to let us know and we will add an annotation if we feel that it is something that can be supported going forwards.

 

Extending a function in a widget is just a case of adding a function of the same name. If you want to invoke the function inherited from the extended widget then simply call this.inherited(arguments) - if you wish to change the arguments that are passed then you should simply include an array of new arguments as an extra argument, for example the DateRange widget extends DateTextBox and extends the setValue function but passes different arguments, as shown here:

 

setValue: function alfresco_forms_controls_DateRange__setValue(value) {
   if (value)
   {
      var valueTokens = value.split(this.dateSeparator);
      this.inherited(arguments, [valueTokens[0]]);
      this.toDate.setValue(valueTokens[1]);
   }
   else
   {
      this.inherited(arguments);
   }
}

 

Widget Life-cycle Functions

Aikau widgets extend from the dijit/_WidgetBase module and the main thing you need to know about this that the postMixInProperties function is called before the HTML template is processed and the postCreate function is called afterwards. These are the 2 key life-cycle functions that you will find yourself extending and in almost all cases you will want to call this.inherited(arguments) to retain the default functionality. The default functions provided by dijit/_WidgetBase are pure life-cycle hooks, but in most cases they will have been implemented by the Aikau widget that you are extending.

 

Summary (so far!)

This is an initial pass at providing some information on extending Aikau widgets - however, if there are use cases that you'd like me to elaborate on further then please let me know (via the comments section) and I will make further updates.

v1 REST APIs with Aikau

Posted by ddraper Oct 25, 2016

My colleague Gavin Cornwell has started writing a series of blog posts on how to make use of the v1 REST APIs that will be available in the next release of Alfresco. PLEASE NOTE: If you want to test out the example in this post then you'll need to follow the setup steps from Gavin's posts!

 

Aikau was originally written to work with the the internal WebScript based REST APIs written for Share. As these REST APIs were not intended for public consumption there is some inconsistency in both the parameters used for the requests and the schema of the response body. This meant that Aikau widgets were intentionally written to be easily configurable to work with different APIs and the use of the services meant that it was easy to normalise any data provided.

 

One of the many great things about Aikau is the fact that it will quite happily work with any of the REST APIs provided by the Alfresco Repository be they WebScripts, CMIS or even the new v1 APIs. This blog post provides a very simple example of how to access and render the data shown in Gavin's second post. The point of the post is to show how to provide a service that can work with the v1 APIs and how to widgets can be configured. At some point in the near future Aikau is likely to start providing dedicated services for working with the v1 APIs (in fact the FileUploadService already can be configured to work with them!). I've stripped the example back to the most basic rendering to focus on how to access the data - the usual principles of building Aikau pages can be applied to make a more interesting and useful interface.

 

Our custom service "blogs/V1RestApiDocumentService" looks like this:

 

define(["dojo/_base/declare",
        "alfresco/services/BaseService",
        "alfresco/core/CoreXhr",
        "alfresco/core/topics",
        "service/constants/Default",
        "dojo/_base/lang"], 
        function(declare, BaseService, CoreXhr, topics, AlfConstants, lang) {
   
   return declare([BaseService, CoreXhr], {

      registerSubscriptions: function blog_V1RestApiDocumentService_registerSubscriptions() {
         this.alfSubscribe(topics.GET_DOCUMENT_LIST, lang.hitch(this, this.onRetrieveDocumentsRequest));
      },

      onRetrieveDocumentsRequest: function blog_V1RestApiDocumentService_onRetrieveDocumentsRequest(payload) {
         var url =  AlfConstants.URL_CONTEXT + "proxy/alfresco-api/-default-/public/alfresco/versions/1/nodes/-root-/children";
         var config = {
            alfSuccessTopic: payload.alfSuccessTopic,
            alfFailureTopic: payload.alfFailureTopic,
            url: url,
            method: "GET",
            callbackScope: this
         };
         this.serviceXhr(config);
      
   });
});

 

The service comprises of two functions registerSubscriptions (which all services should implement) to create the subscription to the topics that the service should respond to - in this case the standard topic for getting documents. The subscription creates the binding to the onRetrieveDocumentsRequest function that actually makes the XHR request for the data.

 

The key thing to note here is how the url variable is constructed - by appending "/proxy/alfresco-api" to the application context (i.e. "/share") then you get authenticated access to the v1 REST APIs (in much the same way that "/proxy/alfresco" will get authenticated access to the WebScript REST APIs). The remainder of the URL (from "-default-" onward) is taken from the example in the blog post.

 

Our custom service mixes in the CoreXhr module to gain access to the serviceXhr function that is called to make the XHR request. The alfSuccessTopic and alfFailureTopic attributes are expected to be provided (which will be handled by the defaultSuccessCallback and defaultFailureCallback functions respectively provided by the CoreXhr module).

 

A simple page model to use this service and render the results could look like this:

 

model.jsonModel = {
   services: ["blog/V1RestApiDocumentService"],
   widgets: [
      {
         name: "alfresco/lists/AlfList",
         config: {
            itemsProperty: "list.entries",
            metadataProperty: "list.pagination",
            startIndexProperty: "list.pagination.skipCount",
            totalResultsProperty: "list.pagination.totalItems",
            widgets: [
               {
                  name: "alfresco/lists/views/AlfListView",
                  config: {
                     widgets: [
                        {
                           name: "alfresco/lists/views/layouts/Row",
                           config: {
                              widgets: [
                                 {
                                    name: "alfresco/lists/views/layouts/Cell",
                                    config: {
                                       widgets: [
                                          {
                                             name: "alfresco/renderers/Property",
                                             config: {
                                                propertyToRender: "entry.name"
                                             }
                                          }
                                       ]
                                    }
                                 }
                              ]
                           }
                        }
                     ]
                  }
               }
            ]
         }
      }
   ]
};

 

It is the AlfList that will make the request to the service - but in order to be able to work with the v1 REST API schema it is necessary to reconfigure:

  • the itemsProperty - to indicate where the array of items can be found in the response
  • the metadataProperty - to indicate where information about the items can be found
  • the startIndexProperty -  for pagination data handling (not used in this example) - to indicate what page
  • the totalResultsProperty - also for pagination data handling

 

Unfortunately we're not able to reuse any of the existing Document Library views (such as AlfSimpleView, AlfDetailedView, etc) because the properties in the v1 REST API response schema are different. So here we're just constructing a simple view to render the name of each node. More information on build views for lists can be found in the Aikau tutorial on GitHub.

 

The end result is not especially exciting, it looks like this:

 

Aikau-V1-REST-API-1.png

... but you should hopefully recognize the rendered values as the names of the folders that you'd find in the alfresco://company/home in the Alfresco Repository.

 

The key thing to takeaway from this post is how easy it is to setup up Aikau to work with the new APIs. In future releases of Aikau we will provide services for working directly with the new APIs, but this should hopefully demonstrate that you can easily write your own services to use them and still make full use of the hundreds of widgets that are currently available in Aikau.

 

Update 26/05/2016

I've done some more experimenting with the V1 REST APIs and made a better user interface (see video below). This video demonstrates some widgets from a development branch of Aikau that has (at the time of writing) yet to be merged into the main project. This branch contains a number of performance improvements as well as a set of widgets with a Material Design Lite skin. You should note that the majority of the existing capabilities of Aikau lists are being re-used and that the interface is driving the browsing, pagination and sorting capabilities of the v1 REST API.

 

Introduction

Form controls in Aikau have always provided a rich and extensible way of validating the data that the user has provided. The fields in a form communicate over the publication/subscription layer to keep track of each others state and the form can only ever be submitted if all the fields report that they are in a valid state. Up until now it has only been able for a field to report that it is invalid in such a way that the form cannot be submitted. However from Aikau 1.0.91 onward it is now possible for a field to configure a validation state that acts purely as a warning.

 

Previous Validation Configuration

Because all Aikau form controls inherit from a common parent they are all able to make use of the same validation configuration. This is done via the validationConfig array. This array can be populated with one of more validations, each processed individually and a field will only report itself as valid if all conditions have been passed. A number of validation handlers are provided by the FormControlValidationMixin module, these are:

 

  • minLength (check that the value has at least a minimum number of characters)
  • maxLength (check that the value does not exceed a maximum number of characters)
  • regex (check that value matches a Regular Expression)
  • validateUnique (check that the identifier is unique by making an XHR request to the Alfresco Repository)
  • validateMatch (check that the value of the field matches that of another field)
  • validationTopic (validate the value by publishing the value to a service)

 

These validation handler are nothing more than functions provided by the FormControlValidationMixin module. This means that it is possible for form controls to provide specific validations that are only relevant for their particular type - for example the DateRange widget provides a validation called validateFromIsBeforeTo and Password provides a validation called confirmMatchingPassword. A custom form control can provide its own validation just by creating a function of that name - the function should accept a validationConfig argument and call the reportValidationResult function with the result, for example:

 

confirmMatchingPassword: function alfresco_forms_controls_Password__confirmMatchingPassword(validationConfig) {
   var isValid = this.confirmationTargetValue === this.getValue();
   this.reportValidationResult(validationConfig, isValid);
}

 

Each validation can accept custom attributes, but all validations it is possible to provide any validation with errorMessage and invertRule attributes that define the error message to display and invert the behaviour of that rule.

 

Example

This is a simple example that shows a TextBox that cannot have a value that is less than 3 characters or more than 5:

 

{
   name: "alfresco/forms/controls/TextBox",
   config: {
      label: "Example",
      description: "Between 3 and 5 chars",
      name: "name",
      value: "",
      validationConfig: [
         {
            validation: "minLength",
            length: 3,
            errorMessage: "Too short"
         },
         {
            validation: "maxLength",
            length: 5,
            errorMessage: "Too long"
         }
      ]
   }
}

 

Asynchronous Validation

The validateUnique and validationTopic are examples of asynchronous validations. They are not expected to return with the result immediately and the will move the form into the invalid state whilst awaiting the response. If the response takes more than a specific amount of time (configured through the _validationInProgressTimeout attribute on the form control) then an "in progress" indicator will be displayed.  The validationTopic validation has also been improved recently to support scoping of responses and configurable validation payloads. This has been done to support a forthcoming feature to perform asynchronous validation of site shortName and title when creating sites via the Aikau SiteService. You will find an example of asynchronous validation in the Search Manager page in Share 5.1 where facet names are validated for uniqueness.

 

Warnings Only

As of Aikau 1.0.91 it is now possible to use the warnOnly attribute to indicate that the validation message should be displayed but that the form submission button should not be disabled. This will be used in the forthcoming release of Share to indicate when a site title has already been used. It is still valid for site titles can be duplicated but it is helpful to alert users to duplicates. This is an example of configuring warnings from the Aikau unit test application:

 

{
   name: "alfresco/forms/controls/TextBox",
   config: {
      fieldId: "TOPIC_VALIDATION",
      label: "Customized validation Topic",
      description: "This simulates validating uniqueness of site identifier",
      value: "test",
      validationConfig: [
         {
            warnOnly: true,
            validation: "validationTopic",
            validationTopic: "ALF_VALIDATE_SITE_IDENTIFIER",
            validationValueProperty: "title",
            validationPayload: {
               title: null
            },
            validateInitialValue: false,
            negate: true,
            validationResultProperty: "response.used",
            errorMessage: "Identifier has been used"
         }
      ]
   }
}

 

The warnings look like this:

AikauValidation1.png

The first text box in the image is configured to validate as an error and the second is configured to validate only as a warning. Note that the validation indicator image is different as is the location and colour of the text. If a form has both a validation error and a validation warning then only the error indicator will be displayed and form submission will be disabled.

 

Summary

Hopefully this provides a useful refresher for Aikau form validation as well as the new features that have been recently added. If you have any questions about this then please ask them in the comments section below.

In Aikau 1.0.91 you'll find that there is an update to the autoSetConfig that could be applied to any form control. The ability to automatically set form control values based on changes to the value of other fields has always been present in Aikau, however it was only possible to configure specific values to set. For example you could do something like this:

 

{
   id: "SOURCE",
   name: "alfresco/forms/controls/Select", 
   config: {
      fieldId: "SOURCE_FIELD",
      label: "Choose from these...",
      name: "source",
      value: "",
      optionsConfig: {
         fixed: [
            { label: "One", value: "1"},
            { label: "Two", value: "2"},
            { label: "Three", value: "3"}
         ]
      }
   }
},
{
   id: "TARGET",
   name: "alfresco/forms/controls/TextBox",
   config: {
      name: "target",
      label: "...to set this",
      autoSetConfig: [
         {
            rulePassValue: "Option 3 Selected",
            ruleFailValue: "",
            rules: [
               {
                  targetId: "SOURCE_FIELD",
                  is: ["3"]
               }
            ]
         }
      ]
   }
}

 

In the above example the TARGET field would automatically have its value set to be "Option 3 Selected" whenever the user selected "Three" from the SOURCE field select menu. This facility was provided to enable hidden fields to be configured and is used effectively for this purpose by the InlineEditProperty renderer.

 

In the course of implementing the Inline Advanced Search we uncovered a use case where it was necessary for one field to automatically copy the value of another.  The reason why you might want to do this is in order to set the same value for multiple parameters. In the inline advanced search example there is the following model snippet:

 

{
   name: "alfresco/forms/controls/Select",
   config: {
      fieldId: "SELECT_FORM",
      label: "Look for",
      description: "This indicates the type of thing that you want to search for",
      name: "itemId",
      optionsConfig: {
         fixed: [
            {
               label: "Content", value: "cm:content"
            },
            {
               label: "Folder", value: "cm:folder"
            }
         ]
      }
   }
},
{
   name: "alfresco/forms/controls/TextBox",
   config: {
      fieldId: "DATA_TYPE",
      name: "formConfig.formSubmissionPayloadMixin.datatype",
      autoSetConfig: [
         {
            copyRule: {
               targetId: "SELECT_FORM"
            }
         }
      ],
      visibilityConfig: {
         initialValue: false
      }
   }
}

 

Here we have a hidden TextBox that is automatically updated to have the value of the advanced search form selected by the user. The autoSetConfig object now supports the copyRule attribute (which is only considered if rulePassValue or ruleFailValue are not provided). The targetId attribute within it indicates the field whose value should be copied.

 

The key difference between these two fields is the name attribute. The hidden field name attribute is targeting the autoSavePublishPayload value (see below). This is necessary in order to ensure that the "datatype" being searched for is included in the search payload.

 

autoSavePublishPayload: {
   itemKind: "type",
   mode: "edit",
   formId: "search",
   alfSuccessTopic: "ADV_SEARCH_FORM_RETRIEVED",
   formConfig: {
      okButtonLabel: "Search",
      okButtonPublishTopic: "ALF_ADVANCED_SEARCH",
      formSubmissionPayloadMixin: {
         alfResponseScope: "ADV_SEARCH_"
      },
      widgetsBefore: [
         {
            name: "alfresco/forms/controls/TextBox",
            config: {
               label: "Keywords",
               name: "searchTerm"
            }
         }
      ]
   }
}

 

This is a great example of how Aikau is able to enhance existing capabilities in a backwards compatible way. If you have any use cases that Aikau fails to address then please feel free to reach out to me either in the comments below, the discussion forums or as an issue on the Aikau GitHub repository.

Inline Advanced Search

Posted by ddraper Oct 20, 2016

Introduction

In a previous blog post I showed how Aikau was beginning to provide support for the XML-based Forms Runtime that Share uses to render forms using the old YUI2 controls. I then wrote additional blogs showing how it could be used to build Aikau pages for Tasks and Data Lists. I've recently looked into how the forms runtime is used to provide the Advanced Search page in Share and have added some updates into the 1.0.91 release to support a new Share extension that you can find here. This is provided as an extension module that was created using the approach described in this blog post.

 

PLEASE NOTE: In order to test this out you'll need at least aikau 1.0.91 JAR as well as the matching version of the aikau-forms-runtime-support JAR. These will need to be placed in your share/WEB-INF/lib folder (the server will need to be restarted).

 

How Advanced Search Works Currently

In order to perform an advanced search in Share it is necessary to go to the Advanced Search page. This allows you to select from a number of search forms configured in the Share forms runtime. When you select a form type ("Content" and "Folder" are the only two provided out-of-the-box) the form is loaded, shown on the page and you can then enter parameters that are relevant for searching for the data type matched to the form.

 

Inline Solution With Aikau

The sample extension provides a way in which advanced search could be used without switching pages. The purpose of this extension is just to show how you can make use of the Aikau FormsRuntimeService to work with existing form configuration. Let's step through the solution..

 

1. Include the FormsRuntimeService on the page

The FormsRuntimeService is not included in the page by default, so we need to add it into the array of services in the model. In the faceted-search.get.js controller extension this can be done as follows:

 

model.jsonModel.services.push("alfresco/services/FormsRuntimeService");

 

2. Add a button for Advanced Search

We're going to allow the user to initiate an advanced search by clicking a button (this isn't the only solution we could have applied, but it's very simple). We can use the "Developer Tools" (described in this blog post) to identify the widget to add the button to and to get the code snippet for finding that widget in the model.

 

var menubar = widgetUtils.findObject(model.jsonModel.widgets, "id", "FCTSRCH_TOP_MENU_BAR");
if (menubar && menubar.config && menubar.config.widgets)
{
   menubar.config.widgets.push(getAdvancedSearchButton());
}

 

3. Create the button definition

In the code snippet above you'll see that we're calling the getAdvancedSearchButton function - we now need to define this function to create a button.

 

function getAdvancedSearchButton() {
   return {
      name: "alfresco/buttons/AlfButton",
      config: {
         label: "Advanced Search",
         additionalCssClasses: "call-to-action"
      }
   };
}

 

4. Make the button request a dialog for the forms

At the moment the button isn't going to do anything because has not been configured with publishTopic or publishPayload attributes. We want to generate a dialog to show the advanced search forms in, so we need to publish on the relevant topic for creating a dialog. The basic configuration would look like this:

 

publishTopic: "ALF_CREATE_DIALOG_REQUEST",
publishPayload: {
   dialogId: "ADVANCED_SEARCH_DIALOG",
   dialogTitle: "Advanced Search",
   contentWidth: "1000px",
   hideTopic: "ALF_ADVANCED_SEARCH"
}

 

This defines the basic configuration for the dialog itself, but provides no information for what should go into it - for that we need to provide a widgetsContent attribute.

 

5. Add some dialog content

We need to be able to switch between the different advanced search forms. When you want Aikau to dynamically update a rendered model it is convenient to use the DynamicWidgets widget. This will update the model displayed based on publications on the topic it is configured to subscribe to:

 

widgetsContent: [
   {
      name: "alfresco/layout/DynamicWidgets",
      config: {
         subscribeGlobal: true,
         subscriptionTopic: "ADV_SEARCH_FORM_RETRIEVED"
      }
   }
]

 

6. Display an initial advanced search form

When the dialog is displayed we want to automatically display a default advanced search form. This can be configured through the publishOnShow attribute. We want to publish on the ALF_FORM_REQUEST topic (which the FormsRuntimeService subscribes to) and provide the necessary information for the form to be rendered. The main attributes are itemId, itemKind, mode and formId. In addition to this we need to override some of the default behaviour.

 

We need to make sure that the DynamicWidgets instance receives the form model to be rendered so the alfSuccessTopic is configured to match the subcriptionTopic previously used.

 

The default behaviour of a form generated by the FormsRuntimeService is to publish to the CrudService, so we need to override that via the formConfig attribute. Here we are changing the button label to be "Search" and the button publish topic to be ALF_ADVANCED_SEARCH (subscribed to by the AlfSearchList already on the page).

 

Interestingly the "Keywords" field that you see on the Advanced Search page is not configured as part of the form so we need to add it in using the widgetsBefore attribute.

 

publishOnShow: [
   {
      publishTopic: "ALF_FORM_REQUEST",
      publishPayload: {
         itemId: "cm:content",
         itemKind: "type",
         mode: "edit",
         formId: "search",
         alfSuccessTopic: "ADV_SEARCH_FORM_RETRIEVED",
         formConfig: {
            okButtonLabel: "Search",
            okButtonPublishTopic: "ALF_ADVANCED_SEARCH",
            formSubmissionPayloadMixin: {
               alfResponseScope: "ADV_SEARCH_",
               datatype: "cm:content"
            },
            widgetsBefore: [
               {
                  name: "alfresco/forms/controls/TextBox",
                  config: {
                     label: "Keywords",
                     name: "searchTerm"
                  }
               }
            ]
         }
      },
      publishGlobal: true
   }
]

 

7. Support advanced search form switching

This configuration would now render an advanced search form, but we want to enable the user to switch between any advanced search form that has been configured. To do this we're going to add an Aikau Form into the widgetsContent array to provide this.

 

Within the form we have a Select widget for choosing the form to display (hard-coded here to provide the out-of-the-box types). The form is configured to automatically save on changes using the autoSavePublishTopic (which publishes the ALF_FORM_REQUEST again) and the autoSavePublishPayload roughly matches that of the publishOnShow configuration previously used.

 

The purpose of the hidden TextBox that copies the selected form value is described in more detail here.

 

{
   name: "alfresco/forms/Form",
   config: {
      autoSavePublishTopic: "ALF_FORM_REQUEST",
      autoSavePublishGlobal: true,
      autoSavePublishPayload: {
         itemKind: "type",
         mode: "edit",
         formId: "search",
         alfSuccessTopic: "ADV_SEARCH_FORM_RETRIEVED",
         formConfig: {
            okButtonLabel: "Search",
            okButtonPublishTopic: "ALF_ADVANCED_SEARCH",
            formSubmissionPayloadMixin: {
               alfResponseScope: "ADV_SEARCH_"
            },
            widgetsBefore: [
               {
                  name: "alfresco/forms/controls/TextBox",
                  config: {
                     label: "Keywords",
                     name: "searchTerm"
                  }
               }
            ]
         }
      },
      widgets: [
         {
            name: "alfresco/forms/controls/Select",
            config: {
               fieldId: "SELECT_FORM",
               label: "Look for",
               description: "This indicates the type of thing that you want to search for",
               name: "itemId",
               optionsConfig: {
                  fixed: [
                     {
                        label: "Content", value: "cm:content"
                     },
                     {
                        label: "Folder", value: "cm:folder"
                     }
                  ]
               }
            }
         },
         {
            name: "alfresco/forms/controls/TextBox",
            config: {
               fieldId: "DATA_TYPE",
               name: "formConfig.formSubmissionPayloadMixin.datatype",
               autoSetConfig: [
                  {
                     copyRule: {
                        targetId: "SELECT_FORM"
                     }
                  }
               ],
               visibilityConfig: {
                  initialValue: false
               }
            }
         }
      ]
   }
}

 

The Final Result

The video below shows the advanced search in action. This is quite an involved use of the FormsRuntimeService but does show how it is beginning to support advanced customization - if you have any use cases that you'd like to see solved then please comment below.

 

Using vue.js with Aikau

Posted by ddraper Oct 7, 2016

Introduction

In the recent State of JavaScript 2016 survey results I noticed that vue.js was gaining massively in popularity as a front-end framework so I thought it might be an interesting experiment to see how easily it could be leveraged within Aikau. I'll admit that I haven't delved to deeply into vue.js in any meaningful way beyond reading the first few pages of the guide so don't expect this to provide any earth-shattering insight into what you can and can't do with it, and I have no real opinion on it yet beyond having noticed it's growing adoption. This post can be considered a more general guide to how you can go about using the JS framework of your choice within Aikau.  In this example I'm going to create an extension that provides a Share dashlet rendering a very simple vue.js component (the obligatory "Hello World" component in fact).

 

Hello Vue.js

The first thing I noticed when going through the guide was how simple it was - write a template and create a component that targets that template. This approach lends itself very nicely to Aikau which breaks widgets down into JS, CSS, templates and localization files. The template for the example looks like this:

 

<div id="app">
  {{ message }}
</div>

 

...and the JavaScript looks like this:

 

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

 

The idea is that the Vue instance uses the el CSS style selector to target the fragment of DOM to convert into a component and substitutes the {{ message }} token with the value of message.

 

Adding a Package Declaration

The other great thing I found out was that vue.js is written to support AMD. This means that we can just declare it as a new package for Aikau to consume. I downloaded the source files and placed them in the META-INF/js/lib folder of my extension JAR and defined a couple of new packages ("vue" for the vue.js libary, and "blog" for my custom components) like this:

 

<extension>
  <modules>
    <module>
      <id>vue.js extension</id>
      <auto-deploy>true</auto-deploy>
      <evaluator type="default.extensibility.evaluator"/>
      <configurations>
        <config evaluator="string-compare" condition="WebFramework" replace="false">
          <web-framework>
            <dojo-pages> 
                <packages> 
                    <package name="vue" location="js/lib" main="vue"/> 
                    <package name="blog" location="js/blog"/> 
                </packages>
            </dojo-pages>
          </web-framework>
        </config>
      </configurations>
    </module>
  </modules>
</extension>

 

Creating a Widget

The next step was to create a widget that made use of the vue.js package. This widget would comprise of the JavaScript file and an HTML template file. The JavaScript file looks like this:

 

define(["dojo/_base/declare",
        "dijit/_WidgetBase", 
        "dijit/_TemplatedMixin",
        "dojo/text!./templates/VueExample.html",
        "alfresco/core/Core",
        "vue"], 
        function(declare, _WidgetBase, _TemplatedMixin, template, AlfCore, Vue) {
   
   return declare([_WidgetBase, _TemplatedMixin, AlfCore], {

      templateString: template,

      postCreate: function vue_VueExample__postCreate() {
         new Vue({
            el: "#" + this.id,
            data: {
               message: "Hello Vue.js!"
            }
         });
      }
   });
});

 

The key things to notice here are that we're importing the "vue" package, and the "VueExample.html" template. You might also notice that the call to create a new Vue component uses a slightly different el attribute - in this case we're just using the id attribute that will be generated for us by Aikau (unless we explicitly provide one)... this is guaranteed to be unique to ensure that vue.js will only work with the right widget instance!

 

The template file is also very similar:

 

<div class="blog_VueExample">
  <p>{{ message }}</p>
</div>

 

...the only difference is that we're specifying a BEM style class for our widget. We use BEM throughout our Aikau widgets to ensure that styles do not "bleed" between widgets inadvertently.

 

Defining the Dashlet

I described in detail how to create an Aikau dashlet in this previous blog post so I won't dwell on it too much - this is the JavaScript controller for defining it:

 

model.jsonModel = {
   rootNodeId: args.htmlid,
   pubSubScope: instance.object.id,
   services: [],
   widgets: [
      {
         name: "alfresco/dashlets/Dashlet",
         config: {
            title: "vue.js example",
            bodyHeight: args.height || null,
            componentId: instance.object.id,
            widgetsForToolbar: [],
            widgetsForBody: [
               {
                  name: "blog/VueExample"
               }
            ]
         }
      }
   ]
};

 

The main thing to note is that the widgetsForBody is where we're placing our new vue.js Aikau widget.

 

The End Result

After building the extension JAR and dropping it into the share/WEB-INF/lib folder and restarting Share the dashlet is available to be added to dashboards and looks like this:

vuejs dashlet.png

 

Summary

OK.... so it's not the most exciting showcase of the capabilities of vue.js, but if you're interested in using vue.js in your Share customizations then using this approach is probably a good place to start. I'll read a bit more into vue.js and see if it's something that we can use more of in Aikau as it does seem like a good fit for providing enhanced templating and binding capabilities. I'd be interested to know people's thoughts on this - please let me know in the comments section if you've tried out vue.js and what you think of it.

 

You can find the source code for this example in this GitHub repository.

Introduction

In my last blog post I showed how easy it is to add new items into the Aikau Document Library "Create Content..." menu. In this particularly example I was showing how to create markdown content but in order to do this I actually needed to create a new widget. It's been suggested in the past that I should try to provide more examples of how to create custom widgets so I figured that it would be as good a time as any...

 

Aikau already had widgets for both editing content with syntax highlighting and previewing markdown but these didn't yet exist as a single entity. The great thing about Aikau is that it incredibly easy to combine existing widgets together to make a new widget that can be referenced as an individual entity. We learned our lessons from the original YUI2 widgets that it is important to define fine-grained widgets that can be easily composed rather than creating large-grained coarse widgets that become increasingly hard to customize over time. The Dojo _WidgetBase module that Aikau builds upon also provides excellent inheritance capabilities that we are able to leverage.

 

So our objective was to create a new widget that allowed editing of markdown and showed a preview of the changes as they happened. As this widget is to be used for creating content submitted through a form then it needed to also inherit from BaseFormControl.  This meant that obvious candidate to extend would be the existing CodeMirrorEditor form control as it had the correct ancestry and could be configured to show markdown syntax highlighting.

 

Creating the Widget

The initial code to extend the widget looks like this:

 

define(["dojo/_base/declare",
        "alfresco/forms/controls/CodeMirrorEditor"],
        function(declare, CodeMirrorEditor) {
   return declare([CodeMirrorEditor], {
   });
});

 

This essentially defines a new widget that is exactly the same as the CodeMirrorEditor, now we can start to change a few things. First we know that we want to use markdown highlighting - this is done by setting the editMode:

 

define(["dojo/_base/declare",
        "alfresco/forms/controls/CodeMirrorEditor"],
        function(declare, CodeMirrorEditor) {
   return declare([CodeMirrorEditor], {
      editMode: "markdown"
   });
});

 

We also know that we want to change how the form control itself is constructed. We do this by extending (through a call to this.inherited(arguments) the createFormControl function:

 

define(["dojo/_base/declare",
        "alfresco/forms/controls/CodeMirrorEditor"],
        function(declare, CodeMirrorEditor) {
   return declare([CodeMirrorEditor], {
      editMode: "markdown",

      createFormControl: function alfresco_forms_controls_MarkdownWithPreviewEditor__createFormControl(config, domNode) {
         var widget = this.inherited(arguments);
         return widget;
      }
   });
});

 

We want to also create the Markdown widget. This requires a new DOM element created, and a new Markdown widget created that binds to it:

 

createFormControl: function alfresco_forms_controls_MarkdownWithPreviewEditor__createFormControl(config, domNode) {
   var widget = this.inherited(arguments);
   this.previewNode = domConstruct.create("div", { 
      style: "height:" + this.height + "px;width:" + this.width + "px;",
      className: "alfresco-forms-controls-MarkdownWithPreviewEditor__previewNode"
   }, this._controlNode);
   var preview = this.createWidget({
      name: "alfresco/html/Markdown",
      config: {
         markdown: this.initialValue
      }
   });
   preview.placeAt(this.previewNode);
   return widget;
},

 

In order for the Markdown widget to show the changes as they happen it is necessary to define a subscription topic that it can uses to listen for changes. By calling the generateUuid function we can create a unique topic to prevent any "cross talk" between widgets.

 

this.generatePreviewTopic = this.generateUuid();

this.previewNode = domConstruct.create("div", { 
   style: "height:" + this.height + "px;width:" + this.width + "px;",
   className: "alfresco-forms-controls-MarkdownWithPreviewEditor__previewNode"
}, this._controlNode);

var preview = this.createWidget({
   name: "alfresco/html/Markdown",
   config: {
      markdown: this.initialValue,
      subscriptionTopics: [this.generatePreviewTopic]
   }
});
preview.placeAt(this.previewNode);

 

Now we need to ensure that when the user makes edits they are published on the generatePreviewTopic that the Markdown widget will be subscribed to. This means extending the onEditorChange function to publish the new value each time it is updated.

 

onEditorChange: function alfresco_forms_controls_MarkdownWithPreviewEditor__onEditorChange(editor, changeObject) {
   this.inherited(arguments);
   this.alfPublish(this.generatePreviewTopic, {
      markdown: this.lastValue
   });
}

 

Now we have an extended version of the CodeMirrorEditor that is configured for markdown editing, with an additional Markdown preview widget that is updated on changes.

 

Summary

Although this doesn't show every change (these can be reviewed in the associate pull request) I've tried to capture the essential elements of how two separate widgets were pulled together to make a new one. The steps I took to create this were obvious to me because of my familiarity with Aikau - but if you need help on getting started creating a new widget then just create a question in the discussion forums and I'll be happy to provide you with suggestions on how to get started.

I recently saw a really interesting blog post from Parashift on creating custom editors for Share. It was interesting because they were showing how to use the showdown.js markdown library as a previewer and that is exactly the same markdown library that Aikau uses for its Markdown widget.

 

I thought it might be an interesting exercise to compare the effort required to create the same editor in Aikau - this isn't to take anything away from what Parashift have shown which is a perfectly valid customization for Share - I just wanted to see what it would be like to do it using Aikau.

 

Full disclose: I had to create a new widget and make some modifications to Aikau to support this blog post, but these changes are all now available in the 1.0.89 release of Aikau

 

I'm going to show this as a customization of the Aikau Document Library (rather than the YUI2 version) and this means that the editor will be displayed inline rather than on a separate page. I've discussed the Aikau Document Library in previous blog posts here and here so I would recommend reading through those to understand how you can customize Share to make use of the Aikau Document Library.

 

The main thing we want to do is to define a new AlfCreateContentMenuItem that can be used to show a dialog for creating markdown content. The doclib.lib.js import file that you'll use to build the model for the Document Library contains the useful helper function generateCreateContentMenuItem. We can call this with configuration for the menu item like this:

 

var createMarkdownMenuItem = generateCreateContentMenuItem({
   menuItemLabel: "Create Markdown", 
   dialogTitle: "Create Markdown", 
   iconClass: "alf-textdoc-icon", 
   modelType: "cm:content", 
   mimeType: "text/markown",
   dialogWidth: "900px",
   contentWidgetName: "alfresco/forms/controls/MarkdownWithPreviewEditor", 
   contentWidgetConfig: { 
      width: 300, 
      height: 250
   }
});

 

The main thing to note here is that we're setting the contentWidgetName to the new MarkdownWithPreviewEditor widget and setting a mimeType to "text/markdown".

 

We now need to ensure that the menu item is included in the Document Library model, this can be easily achieved in the configuration passed to the getDocLib function that is called to build the main model:

 

var docLib = getDocLib({
   siteId: page.url.templateArgs.site, 
   containerId: "documentLibrary",
   additionalCreateContentItems: [createMarkdownMenuItem]
});

 

...and that's it !!

 

When you access the page you'll see the following:

 

The full source of the JavaScript controller for the WebScript is here (the descriptor, template and properties files are the same as in the other blogs posts)...

 

<import resource="classpath:/alfresco/site-webscripts/org/alfresco/share/imports/share-header.lib.js">
<import resource="classpath:/alfresco/site-webscripts/org/alfresco/share/imports/share-footer.lib.js">
<import resource="classpath:alfresco/site-webscripts/org/alfresco/aikau/{aikauVersion}/libs/doclib/doclib.lib.js">

// Get the services and widgets for the header...
var services = getHeaderServices();
var widgets = getHeaderModel(msg.get("aikau.doclib.title"));

// Build a list of services for both the header AND the Document Library...
services = services.concat(getDocumentLibraryServices());

// Create the markdown creation menu item...
var createMarkdownMenuItem = generateCreateContentMenuItem({
   menuItemLabel: "Create Markdown", 
   dialogTitle: "Create Markdown", 
   iconClass: "alf-textdoc-icon", 
   modelType: "cm:content", 
   mimeType: "text/markown",
   dialogWidth: "900px",
   contentWidgetName: "alfresco/forms/controls/MarkdownWithPreviewEditor", 
   contentWidgetConfig: { 
      width: 300, 
      height: 250
   }
});

// Create the Document Library model...
var docLib = getDocLib({
   siteId: page.url.templateArgs.site, 
   containerId: "documentLibrary",
   additionalCreateContentItems: [createMarkdownMenuItem]
});

// Add the Document Library model to the header widgets...
widgets.push(docLib);

// The footer wraps everything...
model.jsonModel = getFooterModel(services, widgets);

// Always include user membership data for any group membership based evaluations...
model.jsonModel.groupMemberships = user.properties["alfUserGroups"];

Filter Blog

By date: By tag: