training/angular-training: Angular training (WIP)

Eric BREHAULT <jenkins-z4DKO/[email protected]> Mon, 31 Jul 2017 07:04:29 -0700 (PDT)
Newsgroups gmane.comp.web.zope.plone.cvs
Message-ID <[email protected]>
Repository: training
Branch: refs/heads/angular-training
Date: 2017-07-31T16:03:52+02:00
Author: Eric BREHAULT (ebrehault) <[email protected]>
Commit: https://github.com/plone/training/commit/8f1d10cf04d222733d2d6358f626c89a0e110aa5

Angular training (WIP)

Files changed:
A .gitignore
A angular.rst

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c61f023
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,20 @@
+*.mo
+*~
+._*
+.Python
+.doctrees
+.vagrant
+*.sublime-project
+*.sublime-workspace
+_build/
+bin/
+build/
+include/
+lib/
+lib64
+local/
+pip-selfcheck.json
+plone_training_config/training/
+presentation/
+log/
+/training.sublime-workspace
diff --git a/angular.rst b/angular.rst
new file mode 100644
index 0000000..8cdef55
--- /dev/null
+++ b/angular.rst
@@ -0,0 +1,845 @@
+Angular SDK for Plone
+=====================
+
+In this part you will:
+
+* Create a static site with Angular,
+* Display contents stored in Plone and retrieved using the REST API,
+* Use the Angular default Plone components and views,
+* Extend those components.
+
+Topic covered:
+
+* Plone Angular SDK.
+
+What is Angular
+---------------
+
+`Angular <https://angular.io/>`_ is a JavaScript framework released in 2016.
+
+- It is component-oriented.
+- It is shipped with RxJS, the Reactive Programming library.
+- It works with TypeScript, a superset of EcmaScript 6, which allows a cleaner coding.
+- It provides a powerful CLI to build projects.
+
+It makes it a very good framework which is both powerful and easy to use. 
+
+.. note::
+
+  Angular was initially known as Angular 2 as AngularJS was its ancestor.
+  Angular and AngularJS are very different, but their name are quite close so when looking for packages, verify carefully which Angular it is compliant with.
+
+What is the Plone Angular SDK
+-----------------------------
+
+The `Plone Angular SDK <https://www.npmjs.com/package/@plone/restapi-angular>`_ is an Angular package (named ``@plone/restapi-angular`` as it belongs to the Plone NPM organization).
+
+It is a high-level integration layer between Angular and the :doc:`./restapi`.
+
+It provides:
+
+- services to dialog with the Plone backend,
+- ready-to-use components (for instance ``<plone-navigation>`` or ``<plone-breadcrumbs>``),
+- traversing.
+
+Traversing
+----------
+
+Traversing is a key feature when working with CMS.
+Angular core, like the other major JS frameworks, uses routing.
+It works perfectly for applications, but it is not applicable for web sites (as the site structure is not predictable).
+
+The Traversal service based on `Angular traversal <https://github.com/makinacorpus/angular-traversal>`_ replaces the default Angular routing. It uses the current location to determine the backend resource (the **context**) and the desired rendering (the **view**).
+
+The view is the last part of the current location and is prefixed by `@@`.
+If no view is specified, it defaults to `view`.
+
+The rest of the location is the resource URL.
+
+Example: `/news/what-about-traversal/@@edit`
+
+When traversing to the location, the resource will be requested to the backend, and the result will become the current context, accessible from any component in the app.
+
+According the values in the `@type` property of the context, the appropriate component will be used to render the view.
+
+.. note::
+
+  We can also use another criteria than `@type` by registring a custom marker (the package comes with an `InterfaceMarker` which marks context according the `interfaces` attribute, which is supposed to be a list. At the moment, the Plone REST API does not expose this attribute).
+
+A new integration approach for Plone
+------------------------------------
+
+Creating pure frontend applications to publish Plone-managed information rather than customizing the Plone web interface has several benefits:
+
+- those web sites look better and fit the expectations of nowaday visitors and customers,
+- they are faster and can easily work offline, which makes them more suitable for mobile,
+- frontend development is more approachable than Plone development, and a constantly growing amount of web developers master this kind of technology.
+
+Installing the development environment
+--------------------------------------
+
+First, we need NodeJS 6.10+. We recommend to use nvm to install NodeJS instead of using your OS-based version.
+
+Install nvm on your system using the instructions and provided script at:
+
+https://github.com/creationix/nvm#install-script
+
+Using nvm we will look up the latest lts version of NodeJS and install it::
+
+  $ nvm ls-remote --lts
+  $ nvm install v6.10
+  $ nvm use v6.10
+
+NodeJS is provided with npm, its package manager, we will use it to install the Angular CLI (ng)::
+
+  $ npm install -g @angular/cli@latest
+
+.. note:: ``-g`` means the CLI will be available globally in our nvm instance.
+
+Initializing a new project
+--------------------------
+
+The CLI allows to initialize a project easily::
+
+  $ ng new training --style=scss
+
+.. note:: ``--style=scss`` indicates we will use SCSS for stylesheets.
+
+If we inspect our newly created ``./training``, we see a default Angular project structure:
+
+- the sources are managed in the ``./src`` folder,
+- the dependencies are declared in ``package.json``,
+- and they are installed in the ``./node_modules`` folder.
+
+We can serve our project locally using the CLI::
+
+  $ cd ./training
+  $ ng serve
+
+The result can be seen on http://localhost:4200.
+
+This development server offers the different features we can expect for a convinient frontend developement environment like autoreload and sourcemaps.
+
+The CLI also allows to run the tests::
+
+  $ ng test
+
+Using and customizing the Angular Plone components
+--------------------------------------------------
+
+Preparing the Plone backend
+***************************
+
+We need a Plone server running the `plone.restapi <http://plonerestapi.readthedocs.io>`_ last version.
+
+We will use a `Plone pre-configured Heroku instance <https://github.com/collective/training-sandbox>`_.
+
+Once deployed, create a Plone site, then go to the :menuselection:`Site Setup --> Add-ons` and Plone RESTAPI :guilabel:`Install`.
+
+Adding the @plone/restapi-angular dependency
+********************************************
+
+::
+
+    $ npm install @plone/restapi-angular --save
+
+The ``@plone/restapi-angular`` and its own dependencies have been installed in our ``./node_modules`` folder.
+
+.. note:: the ``--save`` option ensures the dependency is added in our ``package.json``.
+
+We are now ready to use the Plone Angular SDK.
+
+Connecting the project to the Plone backend
+*******************************************
+
+In ``src/app.module.ts``, load the Plone module and set the backend URL:
+
+.. code-block:: ts
+
+  import { RESTAPIModule } from '@plone/restapi-angular';
+
+  ...
+
+  @NgModule({
+    ...
+    imports: [
+      ...
+      RESTAPIModule,
+    ],
+    providers: [
+      {
+        provide: 'CONFIGURATION', useValue: {
+          BACKEND_URL: 'http://whatever.herokuapp.com/Plone',
+        }
+      },
+    ],
+    ...
+
+.. warning:: Make sure to use ``http`` and not ``https`` because the Heroku web configuration is not set up properly for that.
+
+We have to set up the default Plone views for traversal in ``src/app.component.ts``:
+
+.. code-block:: ts
+
+  import { Component } from '@angular/core';
+  import { PloneViews } from '@plone/restapi-angular';
+
+  @Component({
+    ...
+  })
+  export class AppComponent {
+
+    constructor(
+      private views:PloneViews,
+    ) {
+      this.views.initialize();
+    }
+  }
+
+And we need to insert the Plone view in our main page. Let's change ``src/app.component.html`` that way:
+
+.. code-block:: html
+
+  <traverser-outlet></traverser-outlet>
+
+Now, traversing is active, so we can visit the following links:
+
+- ``http://localhost:4200/front-page``
+- ``http://localhost:4200/news``
+- ``http://localhost:4200/events``
+
+Despite our very bad looking rendering, any content stored in our Plone backend can be requested locally.
+
+The same goes with default views, like:
+
+- ``http://localhost:4200/@@sitemap``
+- ``http://localhost:4200/news/@@search?SearchableText=News``
+
+We are also able to use Plone components provided by the SDK.
+Let's change again ``src/app.component.html``:
+
+.. code-block:: html
+
+  <plone-global-navigation></plone-global-navigation>
+  <plone-breadcrumbs></plone-breadcrumbs>
+  <traverser-outlet></traverser-outlet>
+
+Now we get the main navigation bar and the breadcrumbs. Note the navigation is performed client-side (the page is not reloaded).
+
+Integrating a theme
+-------------------
+
+Integrate Bootstrap
+*******************
+
+Add the bootstrap dependency::
+
+  $ npm install bootstrap-sass@~3.3.7 --save
+
+Create a file to manage our SCSS variables: ``src/variables.scss``
+
+.. code-block:: scss
+
+  $blue: #50c0e9;
+  $lightgrey: #f9f9f9;
+
+Import Bootstrap in our main stylesheet ``src/styles.scss``
+
+.. code-block:: scss
+
+  @import "variables.scss";
+
+  $icon-font-path: "../node_modules/bootstrap-sass/assets/fonts/bootstrap/";
+  @import "../node_modules/bootstrap-sass/assets/stylesheets/_bootstrap.scss";
+
+Override a default Plone component template
+*******************************************
+
+We need to change the template of the global navigation.
+
+First we need to generate a new component::
+
+  $ ng generate component global-navigation
+
+The CLI creates a new folder containing the component implementation, and it declares it in ``src/app/app.module.ts``.
+
+Our global navigation needs to inherit from the Plone's one:
+
+``src/app/global-navigation/global-navigation.component.ts``:
+
+.. code-block:: ts
+
+  import { Component } from '@angular/core';
+  import { GlobalNavigation } from '@plone/restapi-angular';
+
+  @Component({
+    selector: 'app-global-navigation',
+    templateUrl: './global-navigation.component.html',
+    styleUrls: ['./global-navigation.component.scss']
+  })
+  export class GlobalNavigationComponent extends GlobalNavigation {}
+
+And now we can set the template we need:
+
+``src/app/global-navigation/global-navigation.component.html``:
+
+.. code-block:: html
+
+  <nav class="navbar navbar-default" role="navigation">
+    <div class="container-fluid">
+      <div class="navbar-header">
+        <div class="navbar-brand">
+          <a traverseTo="/">
+            <h1>Plone conference</h1>
+          </a>
+        </div>
+      </div>
+      <div class="menu">
+        <ul class="nav nav-tabs" role="tablist">
+          <li *ngFor="let link of links" [ngClass]="{'active': link.active}">
+            <a [traverseTo]="link.path">{{ link.title }}</a>
+          </li>
+        </ul>
+      </div>
+    </div>
+  </nav>
+
+And style it in ``src/app/global-navigation/global-navigation.component.scss``:
+
+.. code-block:: scss
+
+  @import "../../variables.scss";
+
+  .navbar-default {
+    background-color: white;
+    border-radius:0;
+    border-right:0;
+    border-left:0;
+    border-top:0;
+  }
+
+  .container-fluid > .navbar-header {
+    margin-right: 30px;
+    margin-left: 10px;
+    margin-top:20px;
+    border-radius:0;
+  }
+  .navbar-brand {
+    float: left;
+    height: 30px;
+    padding: 15px 15px;
+    font-size: 18px;
+    line-height: 20px;
+    h1 {
+      float: left;
+      line-height:20px;
+      padding: 20px;
+      font-size: 30px;
+      margin-top:-23px;
+      color: $blue;
+      &:hover {
+        background-color:white;
+      }
+    }
+  }
+
+  .menu {
+    font-size:14px;
+    float:right;
+    text-transform:uppercase;
+    font-weight:600;	
+    ul.nav-tabs li {
+      color: black;	
+    }
+  }
+
+  .nav-tabs {
+    border-bottom: 0;
+    & > li {
+      float: left;
+      margin-bottom: 0;
+      & > a {
+        margin-top:20px;
+        margin-bottom:20px;
+        margin-right: 20px;
+        line-height: 1.42857143;
+        border-bottom: 3px solid transparent;
+        border-radius:0;
+        color: black;
+        border-top:0;
+        border-right:0;
+        border-left:0;	
+        & > a:hover {
+          border-color: #eee #eee $blue;
+          color: $blue;
+          border-radius:0;
+          background-color: $lightgrey;
+        }
+      }
+      &.active {
+        & > a,
+        & > a:hover,
+        & > a:focus {
+          color: white;
+          cursor: default;
+          background-color: $blue;
+          border: 0;
+          border-bottom-color: transparent;
+          cursor:pointer;  
+        }
+      }
+    }
+  }
+
+Update the app component markup
+*******************************
+
+Now we can fix the main component markup in ``src/app/app.component.html``:
+
+.. code-block:: html
+
+  <header>
+    <div class="container-fluid">
+      <div class="row">
+        <app-global-navigation></app-global-navigation>
+      </div>
+    </div>
+    <div class="container-fluid">
+      <div class="row">
+        <plone-breadcrumbs></plone-breadcrumbs>
+      </div>
+    </div>
+  </header>
+  <main>
+    <div class="container-fluid">
+      <div class="row">
+        <traverser-outlet></traverser-outlet>
+      </div>
+    </div>
+  </main>
+
+Note we use our custom global navigation component (``app-global-navigation``) but we keep the Plone default breadcrumbs component (``plone-breadcrumbs``) as its markup is fine.
+
+Nevertheless, we might need to style it a little bit, let's do that in ``src/styles.scss``:
+
+.. code-block:: scss
+
+  *[traverseTo], *[ng-reflect-traverse-to] {
+    cursor: pointer;
+  }
+
+  a, a:hover, a:focus {
+    color: $blue;
+  }
+
+  .breadcrumb {
+    background-color: transparent;
+    & > .active {
+      color: black;
+    }
+  }
+
+Creating a custom view for the Talk content-type
+------------------------------------------------
+
+Create the Talk content-type in the backend
+*******************************************
+
+We need to go to our Plone backend, then in :menuselection:`Site Setup --> Dexterity content-types`, we add a new content type named Talk.
+
+We add a text field named ``speaker``.
+
+And we select the following behaviors:
+
+- Lead image
+- Rich text
+
+Then we create a new folder named "Talks" where we add few talks, and we publish them all (including the folder).
+
+Create a view component for talks
+*********************************
+
+We could use the default view to display talks, but it would only display the title and the text, and we would like to also display the image and the speaker.
+
+Let's generate a new component with the CLI::
+
+  $ ng generate component talk
+
+To turn it into a valid view component, there are 3 steps:
+
+- declare it in the module's ``entryComponents``,
+- inherit from a Plone view component,
+- register the view to traversal.
+
+In ``app.module.ts``, we can see the CLI has already added ``TalkComponent`` in ``declarations`` which is mandatory for any Angular component.
+But as a view component is dynamically instanciated (depending on the traversed path), we also need to add it in ``entryComponents``:
+
+.. code-block:: ts
+
+  @NgModule({
+    declarations: [
+      AppComponent,
+      GlobalNavigationComponent,
+      TalkComponent
+    ],
+    entryComponents: [
+      TalkComponent,
+    ],
+    ...
+
+Now let's change ``src/app/talk/talk.component.ts`` to inherit from ``ViewView``:
+
+.. code-block:: ts
+
+  import { Component } from '@angular/core';
+  import { ViewView } from '@plone/restapi-angular';
+
+  @Component({
+    selector: 'app-talk',
+    templateUrl: './talk.component.html',
+    styleUrls: ['./talk.component.scss']
+  })
+  export class TalkComponent extends ViewView {}
+
+And lastly, let's associate this component to the ``talk`` content-type as its default view in ``src/app/app.component.ts``:
+
+.. code-block:: ts
+
+  ...
+  import { Traverser } from 'angular-traversal';
+  import { TalkComponent } from './talk/talk.component';
+
+  @Component({
+  ...
+  })
+  export class AppComponent {
+    constructor(
+      private views: PloneViews,
+      private traverser: Traverser,
+    ) {
+      this.views.initialize();
+      this.traverser.addView('view', 'talk', TalkComponent);
+    }
+  }
+
+The view is now properly set up, let's work on the template in ``src/app/talk/talk.component.html``:
+
+.. code-block:: html+ng2
+
+  <div class="col-md-6">
+    <img [src]="context.image.scales.large.download" alt="Illustration" />
+  </div>
+  <div class="col-md-6">
+    <h1>{{ context.title }}</h1>
+    <p>
+      <span class="glyphicon glyphicon-user"></span>
+      {{ context.speaker }}
+    </p>
+    <div [innerHTML]="context.text.data"></div>
+  </div>
+
+Enable comments
+***************
+
+We want to allow visitor to post comments about the talks.
+
+In the Plone backend, in :menuselection:`Site Setup --> Discussion`, we activate comments globally and we allow anonymous comments.
+And in :menuselection:`Site Setup --> Content types`, we select the Talk type, and we allow comments.
+
+Now in ``src/app/talk/talk.component.html`` we just append:
+
+.. code-block:: html+ng2
+
+  <plone-comments></plone-comments>
+
+Displaying news on the home page
+--------------------------------
+
+We want to display the 3 most recent news on the home page.
+
+First we need a Home component. Let's initialize it properly.
+
+..  admonition:: Solution
+  :class: toggle
+
+    We use the CLI:
+
+    ::
+
+      $ ng generate component home
+    
+    Then we add `HomeComponent` in `entryComponents` in the module.
+
+    We declare it as a view for the `Plone Site` type in `AppComponent`:
+
+    .. code-block:: ts
+
+      this.traverser.addView('view', 'Plone Site', HomeComponent);
+
+We want this component to display the 3 most recent news.
+The ``resource`` service from ``@plone/restapi-angular`` provides a ``find`` method to do that.
+
+Here is the ``HomeComponent`` implementation:
+
+.. code-block:: ts
+
+  import { Component, OnInit } from '@angular/core';
+  import { ViewView } from '@plone/restapi-angular';
+
+  @Component({
+    selector: 'app-home',
+    templateUrl: './home.component.html',
+    styleUrls: ['./home.component.scss']
+  })
+  export class HomeComponent extends ViewView implements OnInit {
+
+    news: any[] = [];
+
+    ngOnInit() {
+      this.services.resource.find(
+        { portal_type: 'News Item' },
+        '/',
+        {
+          sort_on: 'created',
+          sort_order: 'reverse',
+          size: 3,
+        },
+      ).subscribe(res => {
+        this.news = res.items;
+      });
+    }
+  }
+
+We could display those news with a very basic layout like this:
+
+.. code-block:: html+ng2
+
+  <ul>
+    <li *ngFor="let item of news">
+      <a [traverseTo]="item['@id']">{{ item.title }}</a>
+    </li>
+  </ul>
+
+Titles are not enough, it would be better to display images.
+
+The ``find`` method returns "light" search results, with only few metadata.
+By adding the ``fullobjects: true`` parameter, it will retrieve the actual News Item objects,
+including the image:
+
+.. code-block:: ts
+
+      this.services.resource.find(
+        { portal_type: 'News Item' },
+        '/',
+        {
+          sort_on: 'created',
+          sort_order: 'reverse',
+          size: 3,
+          fullobjects: true,
+        },
+      )
+
+.. code-block:: html+ng2
+
+  <ul>
+    <li *ngFor="let item of news">
+      <a [traverseTo]="item['@id']">{{ item.title }}</a>
+      <img [src]="item.image.download" />
+    </li>
+  </ul>
+
+It does work, but what about turning it into a nice slideshow?
+
+First let's implement the logic, we need to manage the currently displayed news,
+and we need to news to provide a ``state`` property set to ``'active'`` or ``'inactive'``.
+
+.. code-block:: ts
+
+  export class HomeComponent extends ViewView implements OnInit {
+
+    news: any[] = [];
+    current = -1;
+
+    ngOnInit() {
+      this.services.resource.find(
+        { portal_type: 'News Item' },
+        '/',
+        {
+          sort_on: 'created',
+          sort_order: 'reverse',
+          size: 3,
+          fullobjects: true,
+        },
+      ).subscribe(res => {
+        res.items.map(item => {
+          item.state = 'inactive';
+          this.news.push(item);
+        })
+        this.current = 0;
+        this.news[this.current].state = 'active';
+      });
+    }
+
+    goTo(index) {
+      this.news[this.current].state = 'inactive';
+      if (index < 0) {
+        index = this.news.length - 1;
+      }
+      if (index == this.news.length) {
+        index = 0;
+      }
+      this.current = index;
+      this.news[this.current].state = 'active';
+    }
+  }
+
+Now let's try it with our basic layout:
+
+.. code-block:: html+ng2
+
+  <div *ngIf="current > -1">
+    <a [traverseTo]="news[current]['@id']">{{ news[current].title }}</a>
+    <img [src]="news[current].image.download" />
+  </div>
+  <span (click)="goTo(current+1)">Next</span>
+
+Good, now let's render it with animations.
+
+We need to import the animation module in ``app.module.ts``:
+
+.. code-block:: ts
+
+  import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+  ...
+    imports: [
+      BrowserModule,
+      BrowserAnimationsModule,
+      ...
+
+We need to declare the states and transition in the component decorator:
+
+.. code-block:: ts
+
+  import {
+    trigger,
+    state,
+    style,
+    animate,
+    transition
+  } from '@angular/animations';
+
+  @Component({
+    selector: 'app-home',
+    templateUrl: './home.component.html',
+    styleUrls: ['./home.component.scss'],
+    animations: [
+      trigger('flyInOut', [
+        state('inactive', style({
+          transform: 'translateX(-100%)'
+        })),
+        state('active', style({
+          transform: 'translateX(0)'
+        })),
+        transition('inactive => active', [
+          animate(200, style({ transform: 'translateX(0)' }))
+        ]),
+        transition('active => inactive', [
+          animate(200, style({ transform: 'translateX(-100%)' }))
+        ])
+      ])
+    ]
+  })
+
+And we need update the markup in ``home.component.html``:
+
+.. code-block:: html+ng2
+
+  <div class="col-md-12 slider">
+    <div *ngFor="let item of news" class="slide"
+      [@flyInOut]="item.state">
+      <img [src]="item.image.download" />
+      <div>
+        <a [traverseTo]="item['@id']">{{ item.title }}</a>
+        <p>{{ item.description }}</p>
+      </div>
+      <i class="next-news glyphicon glyphicon-chevron-right" (click)="goTo(current+1)"></i>
+    </div>
+  </div>
+
+... and the style in ``home.component.scss``:
+
+.. code-block:: css
+
+  @import "../../variables.scss";
+
+  .slider {
+    position: relative;
+    padding: 0;
+    height: 400px;
+    overflow: hidden;
+  }
+  .slide {
+    height: 300px;
+    position: absolute;
+    top: 0;
+    width: 100%;
+    img {
+      width: 100%;
+      height: auto
+    }
+    & > div {
+      position: absolute;
+      top: 60%;
+      left: 66%;
+    }
+    a, p {
+      text-transform: uppercase;
+      text-decoration: none;
+      color: white;
+      background-color: $blue;
+      padding: 1.5em;
+    }
+    a {
+      font-weight: bold;
+      font-size: 120%;
+    }
+    p {
+      margin-top: 3em;
+    }
+    .next-news {
+      color: white;
+      position: absolute;
+      font-weight: strong;
+      right: 10px;
+      top: 10px;
+    }
+  }
+
+And we are done!
+
+Adding quick links in the footer
+--------------------------------
+
+Search
+------
+
+Pushing the Plone configuration from Angular
+--------------------------------------------
+
+Deployment
+----------
+
+Enabling offline
+----------------
+
+SEO
+---
+
+robots.txt and sitemap.xml.gz
+*****************************
+
+Title and meta tags
+*******************
+
+Deploying as a server-side rendered site
+****************************************



------------------------------------------------------------------------------
Check out the vibrant tech community on one of the world's most
engaging tech sites, Slashdot.org! http://sdm.link/slashdot