Doing a board like trello in MDriven Turnkey

In order to do more advanced UI controls in MDriven Turnkey AngularJS you can study how this trello-like-board is constructed.

In order to develop something like this it really helps to install MDriven Turnkey locally. This way you can change and see the effect without ftp’ing a lot of files back and forth.

One important ability you may want to use is to continue to use the data in the cloud – but still have the Turnkey web app locally. I use this ability all the time to debug and develop turnkey. And you can use it to when developing page overrides and advanced controls: MDrivenServerOverride.xml

The file MDrivenServerOverride.xml is placed in your App_Data folder and has this format:

 

<?xml version="1.0" encoding="utf-8"?>
<root>
  <MDrivenServerOverride MDrivenServerPWD="----yoursecretpwd---">https://YourMDrivenTurnkeyApp.azurewebsites.net/__MDrivenServer</MDrivenServerOverride>
</root>
image

Adding a file like this allows you to move where MDriven Turnkey should go to find its MDriven Server. Normally the MDriven Server is placed in a sub-application to the Turnkey-application in a folder named __MDrivenServer (double underscores). But this way I can use a local Turnkey app – and point out the MDrivenServer for the cloud app.

Running the MDriven Turnkey in Visual Studio

Download and import the MDriven Turnkey in your local IIS – or use IIS Express. Then open the website in Visual Studio.

Make sure you have the hang of SSL certificate for your localhost or whatever address you will be using. Enable SSL on site.

Add the MDrivenServerOverride file to your App_Data.

In the WebSite you will find a folder EXT_Scripts. This is where we want to keep our custom scripts.

image

Also please notice the “AppWideAngularScriptIncludes – NotInEffect.html” file. Whenever you see these “- NotInEffect” files it means that you create a copy of that file and remove the “- NotInEffect”  and it will have some meaning. The meaning of file is documented in the file you copied. This file has this content:

<!--  
  This file is referenced like this in the AngularApp.cshtml:


@if (File.Exists(Server.MapPath("/EXT_Scripts/AppWideAngularScriptIncludes.html")))
{
  @Html.Raw(File.ReadAllText(Server.MapPath("/EXT_Scripts/AppWideAngularScriptIncludes.html")))
}


  -----------------
  Intension for this file: To enable you to inject scripts that executes before angular compile - thus making it possible to add directives

  Suggested usage in this file:

   <script src="/EXT_Scripts/BoardComponentScriptsThatUseDirectivesAndSuch.js"></script>
   <script src="/EXT_Scripts/GanttComponentScriptsThatUseDirectivesAndSuch.js"></script>
   <script src="/EXT_Scripts/<YourScript>.js"></script>  
  -->

In this article we are going to add this to the AppWideAngularScriptIncludes file:

<script src="/EXT_Scripts/Board.js"></script>

The application will expect to find a Board.js file in EXT_Scripts and we will get to this soon.
We will also need some markup to replace the Turnkey standard UI. This override markup we put in the EXT_View folder:
image

So for things to work in my view that I will name BoardDemo I will need to add a BoardDemo.cshtml in that folder.

In order to get these files to end up in the correct place but still separating them from the MDrivenTurnkey website I will create a separate project. Open a new instances of Visual Studio and create TypeScript-project (TypeScript is important since Javascript will probably make you explode of frustration):

image

In this project I will set up a post build event – that copies the resulting BoardDemo.cshtml and Board.js files to the correct places in my Turnkey webapp:

image

xcopy  /Y   “$(ProjectDir)BoardDemo.cshtml” “C:\CapableObjectsWush\source\StreamingApp\WebApplication2\Views\EXT_OverridePages\”
xcopy /E /Y  /I “$(ProjectDir)EXT_Scripts” “C:\CapableObjectsWush\source\StreamingApp\WebApplication2\EXT_Scripts”

This is just my suggestion of “best-practice”. This way you separate your control development and can have have it in svn or git by its own.

The model

The model I use to serve up information to show in the board is this:

image

And a viewModel – called DemoBoard:

image

We will come back to this in a while.

We also add a ViewModel that we will use for editing a BoardCard:

image

The markup override

This is the markup and css I will use:

@{ Layout = null; }


<h3>{{root._ViewModelPresentation}}</h3>



<style>

  

  .boardlist {
    vertical-align: top;
  }

  .boardheader {
    font-weight: bold;
    font-size: 1.20rem;
    background-color: seagreen;
    text-align:center;
    color:white;
  }

   .card {
    vertical-align: top;
    background-color: mediumpurple;
    margin: 2px;
    border-radius: 12px;
    border: 2px solid purple;
    padding: 6px;
  }

   .cardname {
    font-weight: bold;
  }

  .cardtext {
    font-size: 0.90rem;
    word-wrap: break-word;
  }

 .rotateWhileMove {
    -webkit-transform: rotate(7deg);
    -moz-transform: rotate(7deg);
    -ms-transform: rotate(7deg);
    -o-transform: rotate(7deg);
    z-index:1000;
  }

  .myHorizontalbut {
    height: 24px;
    width: 90px;
    position: relative;
    padding: 2px;
    display: inline-block;
    margin: 2px;
  }
   
</style>


<div id="theDivForTheBoard" style="height:300px;" ph-board  vmclassid="{{root.VMClassId}}">
  <table border="1" align="center" style="height:100%;width:100%;">
    <tr>
      <td ng-repeat="boardlist in root.BoardLists" height="20px" width="200px">
        <div class="boardheader">{{boardlist.Name}}</div>
      </td>
    </tr>
    
    <tr>
      <td ng-repeat="boardlist in root.BoardLists" class="boardlist" ph-boardlist vmclassid="{{boardlist.VMClassId}}">
        <div ng-repeat="card in boardlist.BoardCards" class="card" ph-card>
          <div class="cardname">{{card.Name}}</div>
          <div class="cardtext">{{card.Text}}</div>
        </div>   
      </td>
    </tr>
   
  </table>
</div>

<button ng-repeat="oneaction in root.VM.StateActions() | orderBy:'+SortKey'"
        ng-class="oneaction.Class" ng-click="oneaction.Execute(); hidemenu();"
        ng-disabled="!oneaction.Enable" class="myHorizontalbut">
  {{oneaction.Presentation}}
</button>


<
img id="loadingAnimation" src="/Content/loadingAnimation.gif" scroll-position="scroll" style="margin-top: 0px;" ng-show="root.VM.Loading()" class="ng-hide">

I do not need to be a superman to know what to write – I can just look at the page source before I override it on what goes where – or I can use <mysite>/MDriven/Development to get documentation on how we do the standard UI:

image

 

The markup gives this UI on data from my model:

image

While working with this I can have the Visual Studio with MDriven Turnkey running – I can watch the result in a browser – and better yet I can use my second Visual Studio to change the override markup.  Then I Compile (this will trigger my post build action) so that the files are copied into the folders of the running turnkey site. Finally I hit refresh in the browser to see the effect of my changes. This loop is fast to work with – and fast is good.

The script

To get the desired behavior to drag a card from one column to another we need to add javascript. Luckily TypeScript is much easier to work with and turns into javascript upon each save. So take my word for it – use TypeScript – do not hack javascript directly. Of course you may do as you please – just saying.

This is the TypeScript source for the Board.ts that will turn into Board.js on each save:

//# sourceURL=EXT_Scripts/Board.js
/// <reference path="../typings/jquery/jquery.d.ts" />



function InstallTheDirective(MDrivenAngularApp) {
  console.trace("InstallTheDirective" + MDrivenAngularApp.toString());

  MDrivenAngularApp.directive('phCard', ['$document', function ($document) {

    return {
      link: function (scope, element, attr) {
        var startX = 0, startY = 0, x = 0, y = 0;

        element.attr("vmclassid", scope.card.VMClassId);
        let bl = scope.$parent.boardlist;

        element.css({
          position: 'relative',
          cursor: 'pointer'
        });

        var clicks = 0;
        element.on('mousedown', function (event) {
          // Prevent default dragging of selected content
          event.preventDefault();
          x = 0;
          y = 0;
          startX = event.pageX - x;
          startY = event.pageY - y;
          clicks++;
          if (clicks == 1) {
            setTimeout(function () {
              if (clicks == 1) {
                $document.on('mousemove', mousemove);
                $document.on('mouseup', mouseup);
                element.addClass("rotateWhileMove");

              } else {
                // double click - execute card action
                scope.card.vCurrent = true;
                var theobjectfortheboard = scope.card.VMClassParent.VMClassParent;
                scope.$root.MDrivenViewModel.Execute(theobjectfortheboard.VMClassType(), "EditCurrentCardAction");

              }
              clicks = 0;
            }, 300);
          }

        });


        function mousemove(event) {
          y = event.pageY - startY;
          x = event.pageX - startX;
          element.css({
            top: y + 'px',
            left: x + 'px'
          });
        }

        function mouseup(event) {

          $document.off('mousemove', mousemove);
          $document.off('mouseup', mouseup);
          y = event.clientY;
          x = event.clientX;

          element.removeClass("rotateWhileMove");
          let actiondone: boolean = false;

          var cardid: string = scope.card.VMClassId;
          var theobjectfortheboard = scope.card.VMClassParent.VMClassParent;
          var theboarddiv = $("[vmclassid='" + theobjectfortheboard.VMClassId + "']");
          if (theboarddiv && scope.card && scope.card.VMClassParent) {
            // find all phBoardList elements under this board in DOM
            var alltheboardlists = $(theboarddiv).find("[ph-boardlist]");
            for (let elem of alltheboardlists.get()) {
              var r = (<HTMLElement>elem).getBoundingClientRect();
              if (r.top < y && r.bottom > y && r.left < x && r.right > x) {
                let thelistWeWereDroppedIn: HTMLElement = elem;
                let vmclassid = thelistWeWereDroppedIn.getAttribute("vmclassid");
                let theobjectforthelist = scope.$root.MDrivenViewModel.GetFromVMClassId(vmclassid);
                if (theobjectforthelist !== scope.card.VMClassParent) {
                  theobjectfortheboard.MoveActionTargetList_AsExternalId = theobjectforthelist.VMClassAsExternalId();
                  theobjectfortheboard.MoveActionTargetCard_AsExternalId = scope.card.VMClassAsExternalId();
                  scope.$root.MDrivenViewModel.CallServerAction(theobjectfortheboard.VMClassType(), "MoveAction");
                  actiondone = true;
                }
              }
            }
          }

          if (!actiondone) {
            element.css({
              top: 'auto',
              left: 'auto'
            });
          }

        }
      }
    };
  }]);

}

console.trace("this file loaded");
InstallTheDirective(angular.module('MDrivenAngularApp'));

The script defines a angularJS directive called “phCard” and that is used in the markup – but in markup you must write “ph-card” (this is the way of angular to ).

In short the script hooks mouse events to allow you to move the card. Once you drop the card the script ends up in mouseup – and here we find out the list you were over on drop – we set a couple of variables – then execute the action “MoveAction”. On the server the MoveAction-action change the owner of the card and Turnkey will be notified about this change – and that will end up as rendering of the card in the new column.

A video of this implementation is available here:

Generic behavior

Notice how the html and views are free from references to the model. The only assumptions the board implementation does is that the ViewModel should have certain properties and actions. So what we have done is actually a generic component that you may apply on any model – as long as you create a ViewModel that honors the names referred to from the markup and the script. This opens up a lot of possibilities. And think about how this can be used in conjunction with snippets that we talked about here . Consider doing a snippet that creates the necessary pattern your reusable component needs. Now you can use boards for many different reasons in the same app – or in different apps… Hmm makes you think does it not?

This entry was posted in AngularJS, Javascript, MDrivenTurnkey, TypeScript and tagged , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

*