Search This Blog

Wednesday, December 7, 2016

Installing and creating the first project with Nativescript and Angular2 on Ubuntu

Nativescript is the new opensource language for cross platform development using Angular 2. The framework and language is new, so took a little time to get things going. This post looks at the following:
  1. Installing the development tools, 
  2. Creating a simple application 
  3. Running the application on emulator and physical device
  4. Packaging the file for distribution.
Be aware that the application is not very sophisticated as the focus is on the development experience rather than the app.

Prereq Installation

Nativescript and Angular2 installation is a little more involved than other nodejs environments. This is primarily due to the Android environment dependencies. Here are the steps.
  • Enable virtualization on your BIOS for the emulators
  • Install prerequisite libraries
  • Install JDK and Android Studio SDK
  • Install Nativescript
The detailed steps are listed in https://docs.nativescript.org/start/ns-setup-linux, but here is the process I followed. I actually created a script to do the whole thing end to end, even though you are better off running step by step. First step on my machine indicated that virtualization had to be enabled at the BIOS level, which I had to do.


Here is the  script for first 3 steps until the end of installing Android Studio and emulators..

# Set the current directory to INSTALL_DIR as we will need to come back here
export INSTALL_DIR="$PWD"

# kvm-ok is needed for the emulators to be installed. It checks if BIOS ..
# supports hardware accelerated KVM virtual machines. You may need to turn on
# hardware accelerated virtual machines in your BIOS
sudo apt-get install cpu-checker
sudo kvm-ok
# sudo apt-get install qemu-kvm libvirt-bin ubuntu-vm-builder bridge-utils

# Install pre-requisite libraries
sudo apt-get install lib32z1 lib32ncurses5 lib32bz2-1.0 libstdc++6:i386
sudo apt-get install g++

# Install JDK
sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update
sudo apt-get install oracle-java8-installer
sudo update-alternatives --config java
export JAVA_HOME=$(update-alternatives --query javac | sed -n -e 's/Best: *\(.*\)\/bin\/javac/\1/p')

# Setup Android
mkdir -p ~/android
cd ~/android
wget -nc https://dl.google.com/android/android-sdk_r24.4.1-linux.tgz
tar -xvf *.tgz
export ANDROID_HOME=~/android/android-sdk-linux
sudo $ANDROID_HOME/tools/android update sdk --filter tools,platform-tools,android-23,build-tools-23.0.3,extra-android-m2repository,extra-google-m2repository,extra-android-support --all --no-ui

If for some reason, ANDROID_HOME is not added to your .bashrc path, we will also need to manually edit our .bashrc file to add the ANDROID_HOME and the Android tools permanently. For this, edit the .bashrc file in your root folder and add the following lines.

To open the .bashrc file


gedit .bashrc

Now add the following lines at the end.


export ANDROID_HOME=$HOME/android/android-sdk-linux
export PATH=$PATH:$ANDROID_HOME/tools
export PATH=$PATH:$ANDROID_HOME/platform-tools

In my execution, the setup could also not find lib32bz2-1.0. Replacing with libbz2-1.0:i386 worked, as shown below.

sudo apt-get install lib32z1 lib32ncurses5 libbz2-1.0:i386 libstdc++6:i386


Also, setting up Android Studio also requires setting up an emulator. Once the basic Android Studio is setup launch Android SDK manager and make sure that all components of your target API version are installed. To launch Android SDK Manager, enter

sudo $ANDROID_HOME/tools/android

The following screen shot was taken while installing the required API modules.



Testing the environment requires you to setup an Android Virtual device (AVD). For my development, I chose Nexus 6 as the prototype device. Android virtual device Manager can be launched by the following command.

$ANDROID_HOME/tools/android avd

Now we can create a new AVD.


Clicking on Create AVD will show a dialog box similar to one below. This shows the options that I selected to create the AVD on my machine.


Upon starting the AVD, I made sure the display is scaled to the correct size as well.


This should start the emulator display as follows:



Nativescript installation

Nativescript installation is straightforward from this point on..


cd $INSTALL_DIR
sudo npm install nativescript -g --unsafe-perm
export PATH=$PATH:~/.node_modules_global/lib/node_modules/nativescript/bin
tns info
tns doctor


Creating a new project

Now that everything is setup we can go ahead and create a new project. In the spirit of my previous posts on Angular2 and Typescript, I decided to name the project ng2-ns1. The following command creates a new project and adds android as one of the target platforms. Developing for iOS unfortunately is not possible on Ubuntu and other Linux variants.


tns create ng2-ns1 --ng
cd ng2-ns1
tns platform add android

This creates an empty typescript based nativescript project with a base app.component.ts and an app.component.html file. I wanted to extend this behaviour by adding two new components, one for a home and another for a login control.

Unlike angular-cli, here the components have to be created by hand. So I did the following in my IDE

  • Added a new folder called home and a folder called login under the app folder.
  • Added files home.component.ts, home.component.html under /home and login.component.ts and login.component.html under /login folders
You can see the folder structure in the following screenshot.


Lets now go through each of these files one by one, starting with the login.component.

login.component.html

Here is the code for the login.component utilizing nativescript UI elements. Notice that:

  • Username and password both use the ngModel directive for two-way binding. This is way more chatty as each keystroke is submitted, but this is a native app, so ideally there is no round trip.
  • The message label uses one way binding
  • The submit button uses an event binding to call a login() function.
  • StackLayout functions like a div tag in html.

<StackLayout>
    <Label text="login" class="h2 text-center"></Label>
    <TextField [(ngModel)]="username" class="" hint="Email Address" keyboardType="email" autocorrect="false" autocapitalizationType="none"></TextField>
    <TextField [(ngModel)]="password"  class="" hint="Password" secure="true"></TextField>
    <Label [text]="message" class="h3 text-center" textWrap="true"></Label>

    <Button text="Login" class="btn btn-primary btn-active" (tap)="login()"></Button>
    <Button text="Sign-up" class="btn"></Button>
</StackLayout>

login.component.ts

This file simply checks for a hard-coded username and password, entered by the user. Important to see a few differences from Angular2 + Typescript for web here.
  • We don't need to decorate the properties as Input or Output properties.
  • We don't create a form element for username and password. Rather, Form submissions are handled through ngModel directive rather than implicit ngForm
  • We throw the loggedIn event using an event emitter once the login is successful.

import {Component, OnInit, EventEmitter} from '@angular/core';

@Component({
    selector: "login",
    templateUrl: "./login/login.component.html",
    styleUrls:["./login/login.component.css"]
})
export class LoginComponent implements OnInit{

    username:string;
    password:string;
    maxtries:number = 3;
    loggedIn = new EventEmitter();
    isLoggedIn:boolean = false;
    loginAttempt: number= 0;

    _valid_user:string = "tom";
    _valid_pwd:string = "jerry";

    constructor() {

    }

    ngOnInit() {
        
    }

    public counter: number = this.maxtries;

    public get message(): string {
        console.log("username="+this.username+", password="+this.password);
        if(this.counter == 0)
        {
            return "Enter username/ password. You have a maximum of " + this.maxtries + " attempts";
        } else if (this.counter > 0 
                && this.isLoggedIn) {
            return "Welcome!";
        } else {
            return "Invalid username/ password. "+ "username="+this.username+", password="+this.password+". "+this.counter + " attempts left";
        }
    }
    
    public login() {
        console.log("Login.login() pressed");
        console.log("username="+this.username+", password="+this.password);
        if ( (this.username == this._valid_user)
                && (this.password == this._valid_pwd)) {
            this.isLoggedIn = true;
            this.loggedIn.emit();
        } else {
            this.counter--;
        }
    }
}

Home component

Next, is the home component that simply is a container for the login component. My intention was to use the *ngIf directive to load or not load the login component, but the nativescript compiler did not like ngIf. I will research more and update this post as neccessary.

home.component.html

This markup simply loads the login component. As mentioned previously, I wanted to include a *ngIf directive to control the display of login component if user was already logged in, but that did not work. I can catch the loggedIn event raised by the login component and set a property of the home component.


<Label text="Version 0.03"></Label>
<login (loggedIn)="onLogin()"></login>

home.component.ts

The home.component.ts file simply has a property that is set once the loggedIn event raised by the login component is caught by the home component.


import { Component, OnInit} from '@angular/core';

@Component({
    selector: "home",
    templateUrl: "./home/home.component.html",
    styleUrls: ["./home/home.component.css"]
})
export class HomeComponent implements OnInit{
    unauthenticatedSession:boolean = true;

    constructor() {
    }

    ngOnInit() {

    }

    onLogin() {
        this.unauthenticatedSession=false;
    }
}

App Component

Finally, we get to the AppComponent. These are also simple.

app.component.html

<StackLayout class="p-20">
    <Label text="Hello Nativescript" class="h1 text-center"> </Label>
    <home></home>
</StackLayout>

app.component.ts

import { Component } from "@angular/core";

@Component({
    selector: "my-app",
    templateUrl: "app.component.html",
})
export class AppComponent {

}

app.module.ts

Among the app classes, the app module is the only one that has some behaviour.

  • We import NativeScriptFormsModule to allow ngModel directives to work. Without this module, the two way binding won't work.
  • We import the AppComponent, HomeComponent and LoginComponent as well.
import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core";
import { NativeScriptModule } from "nativescript-angular/platform";
import { NativeScriptFormsModule } from "nativescript-angular/forms";

import { AppComponent } from "./app.component";
import {HomeComponent} from './home/home.component';
import {LoginComponent} from './login/login.component';

@NgModule({
    declarations: [AppComponent,
                    HomeComponent,
                    LoginComponent],
    bootstrap: [AppComponent],
    imports: [NativeScriptModule,
                NativeScriptFormsModule],
    schemas: [NO_ERRORS_SCHEMA]
})
export class AppModule { }

Running the program

I ran the program on the emulator as well as a physical Samsung S5 connected via USB to my machine. Both cases the program ran successfully, and the physical phone is much faster for testing quickly. The commands are as follows:


# Run the program on emulator
tns run android --emulator

#Run the program on emulator in livesync mode to allow app to refresh on code change
tns livesync android --emulator --watch

# Run the program on physical device
tns run android

#Run the program on physical device in livesync mode
tns livesync android --watch

Here is the application running on the Emulator



Here is the same app running on a Galaxy S5 physical device


Packaging and distribution

Finally, we come to the task of packaging file for distribution. It is done in 3 easy steps
  • Prepare the environment
  • Generate a keystore
  • Build the final distribution
#Prepare the environment
tns prepare android

#Generate a keystore
keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias some_alias

# Build
tns build android --release --key-store-path $PWD/my-release-key.jks --key-store-password some_password --key-store-alias some_alias --key-store-alias-password some_password

This creates a build as shown below


Finally, we can publish the app on Google Play, by signing in to https://play.google.com/apps/publish/signup/ and paying $25.


That's it!


Sunday, December 4, 2016

Drag and Drop in Angular 2 with ng2-dragula

I wanted to implement a drag and drop scenario using Angular 2 and decided to put ng2-dragula through its paces. In this post, I will take this library for a spin. The post will follow the following steps
  1. Creating a new project to utilize ng2-dragula.
  2. Configuring the project to use ng2-dragula
  3. Implementing the dragula library
  4. Adding more complex logic to the process to test dragula capabilities.
Lets go through these steps.

Creating a new project and install ng2-dragula

The first step had several tasks.. The high-level description of the tasks is as follows
  • Create a new project using angular-cli. 
  • Change into the root folder of the newly created directory
  • Install angular/material library using the node package manager (npm). Record this in the package.json using --save command.
  • Install the ng2-dragula and dragula libraries using npm. 
  • Generate a new component - myeditor that will use the dragula library
ng new ng2-4
cd ng2-4
npm install --save @angular/material
npm install --save ng2-dragula dragula
ng generate component myeditor

The big issue is that as time of writing this post, ng2-dragula expected a previous version of angular2 that shows up as unmet peer dependencies. I could not resolve it, but code seemed to work for most part.

Configuring the project to use ng2-dragula

Now that the library is installed, we need to configure the project to use ng2-dragula. This implies the following steps
  • Edit the app.module.ts file and import the DragulaModule from ng2-dragula/ng2-dragula
  • Indicate DragulaModule import in the module declarations so it is accessible to other components in the project. (Note that DragulaModule is a wrapper on a provider called DragulaService and a directive called DragulaDirective within the package).
  • Add a vendor.ts under the /src directory and add a css import

app.module.ts

import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import {MaterialModule} from '@angular/material';

import { AppComponent } from './app.component';
import { MyeditorComponent } from './myeditor/myeditor.component';
import { DragulaModule } from 'ng2-dragula/ng2-dragula';


@NgModule({
  declarations: [
    AppComponent,
    MyeditorComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    MaterialModule.forRoot(),
    DragulaModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Here is a screenshot from my environment



This vendor.ts file only contains an import of a css class as per the documentation but the css import was the most erratic part of the whole process, and I am not sure I got it completely right. Still, I followed the steps in the documentation.

vendor.ts

import 'dragula/dist/dragula.css';

Here is the screenshot from my environment.


Implementing the dragula library

This is the fun part. Implementing dragula is fairly easy. It requires a some css classes to be added to the component css. In the html we simply define the div tags that will define the different boxes and add the dragula directive. The code takes it from there.

Here are the high level steps:

  • In the component html file, we define a simple html that contains an outer div tag which has a class called "wrapper" and 4 inner divs each that have a css class called "container".
  • Each of the div.container elements implements the [dragula]  directive with a specific name - "editor-bag" that indicates that their contents can be dragged within them or between other div.containers which implement the same directive with same bag name.

myeditor.component.html


<div class="wrapper">
  <div class="container master" [dragula]="'editor-bag'">
    <div class="A Z">A</div>
    <div class="B Z">B</div>
    <div class="C Z">C</div>
    <div class="D Z">D</div>
    <div class="E Z">E</div>
  </div>
  <div class="container" [dragula]="'editor-bag'">
    <div class="A Z">A</div>
  </div> 
  <div class="container" [dragula]="'editor-bag'">
  </div> 
  <div class="container" [dragula]="'editor-bag'">
  </div> 
</div>

The accompanying component css class adds some rendering logic including some of the behavior prescribed by the dragula folks. Important things to note are

  • css classes "wrapper" and "container" both implement display:float
  • css class Z ensures that all boxes labeled A through E are rectangles of a certain size and can be dragged around.

myeditor.component.css



*, *:before, *:after {
  -webkit-box-sizing: inherit;
  -moz-box-sizing: inherit;
  box-sizing: inherit;
}

.promo {
  margin-bottom: 0;
  font-style: italic;
  padding: 10px;
  background-color: #ff4020;
  border-bottom: 5px solid #c00;
}

.parent {
  background-color: rgba(255, 255, 255, 0.2);
  margin: 50px 0;
  padding: 20px;
}

.gh-fork {
  position: fixed;
  top: 0;
  right: 0;
  border: 0;
}

.wrapper {
  display: flex;
  background-color: #942A57;
  flex-flow: row wrap;
}
.container {
  display: flex;
  background-color: rgba(255, 255, 255, 0.2);
  width: 100%;
  min-height: 150px;
  min-width: 500px;
  margin-left: 50px;
  margin-right: 50px;
  margin-top:20px;
  margin-bottom:20px;
}
.container:nth-child(odd) {
  background-color: rgba(0, 0, 0, 0.2);
}

.container div,
.gu-mirror {
  margin: 10px;
  padding: 10px;
  background-color: rgba(0, 0, 0, 0.2);
  transition: opacity 0.4s ease-in-out;
}
.container div {
  cursor: move;
  cursor: grab;
  cursor: -moz-grab;
  cursor: -webkit-grab;
}
.gu-mirror {
  cursor: grabbing;
  cursor: -moz-grabbing;
  cursor: -webkit-grabbing;
}
.container .ex-moved {
  background-color: #e74c3c;
}
.container.ex-over {
  background-color: rgba(255, 255, 255, 0.3);
}
.handle {
  padding: 0 5px;
  margin-right: 5px;
  background-color: rgba(0, 0, 0, 0.4);
  cursor: move;
}
nested-repeat-example .container span {
  display: block;
  padding: 8px;
}
.container.master {
    background-color:rgba(155,155,155,1);
    color: white;
}

.Z {
  width: 75px;
  height: 125px;
  background-color: floralwhite;
}

This allows the boxes located in the first row to be moved around to any of the rows in the screen and their relative position to each other can also be modified.

The logic in the app.component.ts is fairly simple as its just the container for the myeditor component.

app.component.ts

<h1>
  {{title}}
</h1>
<myeditor></myeditor>

Finally, app.component.css includes remaining css classes.


body {
  background-color: #FFF9C4;
  margin: 0 auto;
 
}

html, body {
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
}

*, *:before, *:after {
  -webkit-box-sizing: inherit;
  -moz-box-sizing: inherit;
  box-sizing: inherit;
}

body, input, button {
  font-family: Georgia, Helvetica;
  font-size: 17px;
  color: #ecf0f1;
}

h1 {
  text-align: center;
  background-color: #AC5C7E;
  margin-top: 20px;
  margin-bottom: 0;
  padding: 10px;
}

h3 {
  background-color: rgba(255, 255, 255, 0.2);
  border-bottom: 5px solid #A13462;
  text-align: center;
  padding: 10px;
}

h3 div {
  margin-bottom: 10px;
}

Adding more complex logic

Naturally, this was not complex enough logic. I wanted to extend the logic to ensure..
  • The boxes in the top row (that also implements the css class "master") can be cloned to any other rows, but boxes in top row can never be re-arranged.
  • Boxes in the bottom rows can be re-arranged among themselves but cannot be transferred back to the top row.
  • There are some rules that are followed in positioning the boxes relative to each other.
    • Box A can only be one at the start of the row
    • Box B and D can be anywhere in the row, in any number or any sequence.
    • Box B or D cannot follow Box C.
    • Box C or E can be the last boxes in the row, but there can be no other box after box E.
I decide to implement the logic using different events thrown by the dragula library. I translated the above rules to a setOptions method called within the constructor of the component class.



import { Component, OnInit, Input } from '@angular/core';
import {DragulaService, DragulaDirective} from 'ng2-dragula/ng2-dragula';


@Component({
  selector: 'myeditor',
  templateUrl: './myeditor.component.html',
  styleUrls: ['./myeditor.component.css']
})
export class MyeditorComponent implements OnInit {

  static _debug:boolean = false;
  _debug:boolean = MyeditorComponent._debug;

  static _siblingMap: Map<string, AllowedSiblings> ;

    A_prev: string[] = [];
    A_next: string[] = ["B","C","D","E"];
    B_prev: string[] = ["A","B","D"];
    B_next: string[] = ["B","C","D","E"];
    C_prev: string[] = ["A","B","D"];
    C_next: string[] = ["E"];
    D_prev  = ["A","B","D"];
    D_next: string[] = ["B","C","D","E"];
    E_prev  = ["A","B","C","D"];
    E_next: string[] = [];

  constructor(private dragulaService: DragulaService) { 
    if(MyeditorComponent._siblingMap == null)
        {
          this.setupSiblingMap();
        }
    dragulaService.setOptions('editor-bag',{
      isContainer: function(el) {
        return false;
      },
      moves: function(el, container, handle) {
        return true;//handle.classList.contains('master');
      },
      accepts: function(el, target, source, sibling) {
        var fn_debug = true;
        var acceptAll = false;
        if(!acceptAll)
        {
          if(this._debug || fn_debug) {
            console.log("accepts() start el, target, source, sibling");
            console.log({el,target,source,sibling});
          }
          if(target.classList.contains('master')){
            return false;
          }
          if(sibling==null) {
            return (target.children.length == 0);
          }
          var name:string = el.innerText;
          return MyeditorComponent.areAllowedSiblings(name,sibling);
        }
        return acceptAll;
      },
      invalid: function (el, handle) {
        return false; // don't prevent any drags from initiating by default
      },
      direction: 'vertical',             // Y axis is considered when determining where an element would be dropped
      copy: function(el,source) {
        if(this._debug) {
          console.log("copy() start");
          console.log(el);
          console.log(source);
          console.log("copy() stop");
        }
        return source.classList.contains('master');
      },                       // elements are moved by default, not copied
      copySortSource: false,             // elements in copy-source containers can be reordered
      revertOnSpill: false,              // spilling will put the element back where it was dragged from, if this is true
      removeOnSpill: true,              // spilling will `.remove` the element, if this is true
      mirrorContainer: document.body,    // set the element that gets mirror elements appended
      ignoreInputTextSelection: true     // allows users to select input text, see details below
    })
  }

  ngOnInit() {
    
     this.dragulaService.drag.subscribe((value:any) => {
         if(this._debug) {
          console.log("drag start");
          console.log(value);
          console.log("drag stop");
          console.log(`drag: ${value[0]}`);
         }
         this.onDrag(value.slice(1));
    });

    this.dragulaService.drop.subscribe((value:any) => {
      console.log(`drop: ${value[0]}`);
      this.onDrop(value.slice(1));
    });
    
    this.dragulaService.over.subscribe((value:any) => {
       if(this._debug) { console.log(`over: ${value[0]}`);}
      this.onOver(value.slice(1));
    });
    
    this.dragulaService.out.subscribe((value:any) => {
       if(this._debug) {console.log(`out: ${value[0]}`);}
      this.onOut(value.slice(1));
    });
  }

  private hasClass(el:any, name:string):any {
    return new RegExp('(?:^|\\s+)' + name + '(?:\\s+|$)').test(el.className);
  }

  private addClass(el:any, name:string):void {
    if (!this.hasClass(el, name)) {
      el.className = el.className ? [el.className, name].join(' ') : name;
    }
  }

  private removeClass(el:any, name:string):void {
    if (this.hasClass(el, name)) {
      el.className = el.className.replace(new RegExp('(?:^|\\s+)' + name + '(?:\\s+|$)', 'g'), ' ');
    }
  }

  private onDrag(args:any):void {
    let [e] = args;
    this.removeClass(e, 'ex-moved');
  }

  private onDrop(args:any):void {
    let [e] = args;
    this.addClass(e, 'ex-moved');
  }

  private onOver(args:any):void {
    let [el] = args;
    this.addClass(el, 'ex-over');
  }

  private onOut(args:any):void {
    let [el] = args;
    this.removeClass(el, 'ex-over');
  }

  private static areAllowedSiblings(name:string,sibling:any):boolean {
    // return true;
    var fn_debug: boolean = false;
    var isValid:boolean = true;
  
    var nextSibling = sibling.nextSibling;
    var prevSibling = sibling.previousSibling;
    var allowedSiblings: AllowedSiblings = null;
    var debugMsg = "";
    if( MyeditorComponent._siblingMap!=null) {
      debugMsg+=("1:"+isValid);
      if(isValid) {
          allowedSiblings = MyeditorComponent._siblingMap.get(name);
          isValid = (allowedSiblings!=null);
          if(this._debug || fn_debug) {
            console.log("allowedSiblings");
            console.log(allowedSiblings);
          }
          debugMsg+=(",2:"+isValid);
      }
      if(isValid) {
          if(this._debug || fn_debug) {
            console.log("nextSibling");
            console.log(nextSibling);
          }
        isValid = MyeditorComponent.isAllowedSibling(nextSibling,allowedSiblings.nextSiblings);
        debugMsg+=(",3:"+isValid);
      }
      if(isValid) {
          if(this._debug || fn_debug) {
            console.log("prevSibling");
            console.log(prevSibling);
          }
        isValid = MyeditorComponent.isAllowedSibling(prevSibling,allowedSiblings.prevSiblings);
        debugMsg+=(",4:"+isValid);
      }
    }
    if(this._debug || fn_debug) {
      console.log("isValid:"+ debugMsg);
    }
    fn_debug = false;
    return isValid;
  }

  private static isAllowedSibling(siblingNode, allowedSiblingArray: string[]):boolean {
    var isValid:boolean = false;
    if(siblingNode == null)
    {
      return true;
    }
    if(siblingNode.nodeName == "#text")
    {
      return true;
    }
    if (allowedSiblingArray.indexOf(siblingNode.innerText)>-1)
    {
      isValid=true;
    }
    return isValid;
  }

  private setupSiblingMap() {
    MyeditorComponent._siblingMap = new Map<string, AllowedSiblings>();
  
    MyeditorComponent._siblingMap.set("A", new AllowedSiblings("A",this.A_prev,this.A_next ));
    MyeditorComponent._siblingMap.set("B", new AllowedSiblings("B",this.B_prev,this.B_next ));
    MyeditorComponent._siblingMap.set("C", new AllowedSiblings("C",this.C_prev,this.C_next ));
    MyeditorComponent._siblingMap.set("D", new AllowedSiblings("D",this.D_prev,this.D_next ));
    MyeditorComponent._siblingMap.set("E", new AllowedSiblings("E",this.E_prev,this.E_next ));
    // if(this._debug) {
    if(true) {
      console.log(MyeditorComponent._siblingMap);
    }
  }
}

export class AllowedSiblings {
  @Input() name:string;
  @Input() prevSiblings: string[];
  @Input() nextSiblings: string[];

  constructor(name:string,
              prevSiblings: string[],
              nextSiblings: string[]) {
    this.name=name;
    this.prevSiblings=prevSiblings;
    this.nextSiblings=nextSiblings;
  }

}

The behavior was only partly implemented correctly. Only the following rules seem to be working.
  • The boxes in the top row (that also implements the css class "master") can be cloned to any other rows, but boxes in top row can never be re-arranged.
  • Boxes in the bottom rows can be re-arranged among themselves but cannot be transferred back to the top row.
  • There are some rules that are followed in positioning the boxes relative to each other.
    • Box A can only be one at the start of the row
    • Box B and D can be anywhere in the row, in any number or any sequence.
    • Box C or E can be the last boxes in the row, but there can be no other box after box E.
These are the errors in the code:

  • No Box can be added after Box A in second row
  • Box C and E cannot be added to any row
Here is a screenshot of the application. The highlighted cells are the ones that were added/ moved.


I will continue to tweak the code and post updates as I fix more errors, or add any explanation.