Archiv der Kategorie: Technik

Feierabendprojekte

Abends, wenn alles schläft, wird Code produziert. Viel Code. Guter Code (?). Rätsel im General Anzeiger, langsame Displays, Roboter, Zettelkästen, Star field simulations, Rechnungsteiler, Spiele, Experimente, Literaturhelfer, technische Spielereien, Open-Source-Projekte und Rapid-Prototyping für Webseiten oder Apps. Die Seite auf der dieser ganze Code gesammelt wird, findet sich hier. Sie ist so was von statisch und schnell, dass einem die Spucke trocknet. Könnte nicht das ganze Netz so einfach sein?

Learning nestjs I

It starts quite simple. Once you have installed the nest CLI, you can issue a command like

nest new tutorial1

This will scaffold a new nestjs application in the directory „tutorial1“. Enter the directory. Then the app can directly be started using

npm run start:dev

If you visit then localhost:3000 you can see „Hello World!“ which is the standard output of the application. Nowadays everything needs to be stored in databases. So let’s install typeorm using sqlite.

npm install --save @nestjs/typeorm typeorm sqlite3

The next step seems to be the creation of a file called ormconfig.json in the applications root directory („tutorial1“ in our case). We’ll paste the following content:

[{
    "name": "default",
    "type": "sqlite",
    "database": "./data/test.sqlite",
    "entities": ["dist/**/*.entity{.ts,.js}"],
    "synchronize": true
}]

This will tell typeorm to use an sqlite database located in directory data/ and apply its magic for all entity files we will create. This has to be known to the app module. We alter the app.module.ts file to look like:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [TypeOrmModule.forRoot()],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

This will deal with the ormconfig.json file as stated here. If your command „npm run start:dev“ is still running you will now have a file called „data/test.sqlite“.

It is time now to decide what the application will be about. Of course this first tutorial will deal with photos and albums (as all nestjs examples seem to do (leaving alone cats and heros)).

nest generate module Photos
nest generate service Photos
nest generate controller Photos

So let’s create a photo as an entity description in „src/photos/photo.entity.ts“.

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Photo {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 500 })
  name: string;

  @Column('text')
  description: string;

  @Column()
  filename: string;

  @Column('int')
  views: number;

  @Column()
  isPublished: boolean;
}

To fully unleash the automagical powers of the mixture of typeorm and nestjs we can make use of the crud controllers.

npm i --save @nestjsx/crud @nestjsx/crud-typeorm typeorm class-transformer class-validator

Since now I’m already lost a bit – what does that all provide ? – we will add the nestjs swagger integration to keep track of the routes all that magix will create for us.

npm install --save @nestjs/swagger swagger-ui-express

After this „main.ts“ needs to know about swagger.

import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);


  const options = new DocumentBuilder()
    .setTitle('Photos example')
    .setDescription('The photos API description')
    .setVersion('1.0')
    .addTag('photos')
    .build();
  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('api', app, document);

  await app.listen(3000);
}
bootstrap();

If you now visit http://localhost:3000/api all routes of your application will be shown. None, except „/“ that displays „Hello world!“ using the app controller.

I now tried to follow the steps of this documentation to add the crud wiring to the photo. It seems that swagger 4.0.9 is broken. So let’s revert (Hmmpf not commited yet :-/ )

npm uninstall --save @nestjs/swagger swagger-ui-express

and changing back main.ts to

import { NestFactory } from '@nestjs/core';
//import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);


  // const options = new DocumentBuilder()
  //   .setTitle('Photos example')
  //   .setDescription('The photos API description')
  //   .setVersion('1.0')
  //   .addTag('photos')
  //   .build();
  // const document = SwaggerModule.createDocument(app, options);
  // SwaggerModule.setup('api', app, document);

  await app.listen(3000);
}
bootstrap();

solves the problem temporarily.

The changes I have done so far are:

// photos.controller.ts

import { Controller } from '@nestjs/common';
import { Photo } from './photo.entity';
import { Crud } from '@nestjsx/crud';
import { PhotosService } from './photos.service';

@Crud({
    model: {
      type: Photo,
    }
})
@Controller('photos')
export class PhotosController  {
    constructor(public service :PhotosService){}
}

// phots.module.ts

import { Module } from '@nestjs/common';
import { PhotosService } from './photos.service';
import { PhotosController } from './photos.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Photo } from './photo.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Photo])],
  providers: [PhotosService],
  controllers: [PhotosController]
})
export class PhotosModule {}

// photos.service.ts

import { Injectable } from '@nestjs/common';
import { TypeOrmCrudService } from '@nestjsx/crud-typeorm';
import { Photo } from './photo.entity';
import { InjectRepository } from '@nestjs/typeorm';

@Injectable()
export class PhotosService extends TypeOrmCrudService<Photo> {
    constructor(@InjectRepository(Photo) repo) {
        super(repo);
      }
}

Since now I’m not able to show the routes that have been created using swagger, you can test that you will get an empty JSON response here.

Time to add some frontend. To make it simple we will just create a static handler and use parceljs to build the frontend. Because everything is bundled so nicely another package is needed.

npm install --save @nestjs/serve-static

We will follow these steps and add to „app.module.ts“:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PhotosModule } from './photos/photos.module';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';

@Module({
  imports: [
    TypeOrmModule.forRoot(), 
    PhotosModule,
    ServeStaticModule .forRoot({
      rootPath: join(__dirname, '..', 'client-dist'),
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

No „client-dist“ directory exists so far. We can now install parcel

npm install -g parcel-bundler

Next I’ll create a folder structure

client/
├── index.html
├── main.css
└── main.js

Where index.html contains

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Photos</title>
    <link rel="stylesheet" href="main.css">
</head>

<body>
    <script src="main.js"></script>
</body>

</html>

Parcel can now be executed in another shell:

parcel client/index.html --out-dir client-dist

Now we delete the „Hello world!“ stuff from app.controller.ts

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}
}

If you now visit http://localhost:3000 you will see an emtpy page with the title Photos. Let us add some content to „client/main.js“

import m from 'mithril';

m.mount(document.body, {
    view: vnode => m('h1', 'Frontend')
});

Once we save the file, mithril will be installed as a dependency. What I personally don’t like at this point is that in package.json frontend and backend are mixed up and that we now need to add CORS to the backend to be able to use hot-reload in the frontend as well. We can do that by adding this line to „main.ts“.

app.enableCors({origin:'localhost:1234'});

A service in „main.js“ that fetches the photos rounds up our work until here.

import m from 'mithril';

const crudService = url => {
    return {
        list: cb => m.request({ url, method: 'GET', id: 0 }).then(cb)
    };
};

const photoService = crudService('http://localhost:3000/photos');

let photos = [];

photoService.list(photos_ => photos = photos_)

m.mount(document.body, {
    view: vnode => [
        m('h1', 'Frontend'),
        m('pre', JSON.stringify(photos, null, 2))
    ]
});

Now visit http://localhost:1234 to see the frontend, displaying an empty array.

This was the first part. It will be continued somewhen.

Splitter

Eine weitere kleine Helfershelferanwendung. Wie oft habe ich schon mit Zettel und Stift ausgerechnet, wer nach einem gemeinsamen Urlaub wieviel zu löhnen hat?

Ob es gut ist, wenn es für alles ein App gibt? Meinen grauen Zellen graut es schon davor, wenn sie dann nichts mehr zu tun haben.

Hier liegt der Code. Hier ist Applikation live zu sehen.

Die Nutzerdaten werden im Browserspeicher gespeichert. Damit man trotzdem eine Abrechnung an die Freunde schicken kann, kann man die Daten auch als Link an die Freunde schicken. Ein Beispiel ist eine fiktive Abrechnung eines Segeltörns.

Keinerlei Daten werden auf dem Server gespeichert.
Alle Informationen liegen allein in der Linkadresse, die dadurch ziemlich lang werden kann. Hier die Adresse vom Segeltörnbeispiel:

https://abulvenz.github.io/splitter/?data=eyJ1c2VycyI6W3sibmFtZSI6IkZhYmkifSx7Im5hbWUiOiJBbmRpIn0seyJuYW1lIjoiVGVkZHkifSx7Im5hbWUiOiJUb2JpIn1dLCJleHBlbnNlcyI6W3sidGl0bGUiOiJCaWVyIiwiYW1vdW50IjoxMzUsInVzZXIiOiJUb2JpIiwidXNlcnMiOlsiRmFiaSIsIkFuZGkiLCJUZWRkeSIsIlRvYmkiXX0seyJ0aXRsZSI6IlNlZ2Vsc2NoZWluIiwiYW1vdW50IjoxMjUwLCJ1c2VyIjoiVGVkZHkiLCJ1c2VycyI6WyJUZWRkeSJdfSx7InRpdGxlIjoiQWxkaWVpbmthdWYiLCJhbW91bnQiOjY1LCJ1c2VyIjoiRmFiaSIsInVzZXJzIjpbIkZhYmkiLCJBbmRpIiwiVGVkZHkiLCJUb2JpIl19LHsidGl0bGUiOiJCb290c2NoYXJ0ZXIiLCJhbW91bnQiOjEyNTUsInVzZXIiOiJUZWRkeSIsInVzZXJzIjpbIkZhYmkiLCJBbmRpIiwiVGVkZHkiLCJUb2JpIl19LHsidGl0bGUiOiJTcHJpdCIsImFtb3VudCI6MTIwLCJ1c2VyIjoiVG9iaSIsInVzZXJzIjpbIkZhYmkiLCJBbmRpIiwiVGVkZHkiLCJUb2JpIl19XSwiY3VycmVuY3kiOiLigqwifQ%3D%3D

Allerdings kann man zum Beispiel bei der Partei „Die Partei“ die Daten ablegen (https://fckaf.de/Sx9), indem man einen Kurzlink erzeugt. Mit dieser Methode kann die gesamte Abrechnung auf drei Zeichen reduziert werden.

Rapid prototyping of Single Page Applications

Using Mithril and ParcelJS

The goal of this article is to show my current way of fast building a prototype of web frontends, that I usually use to try out things or to create tiny games. There is no repository attached. This is a tutorial. Maybe you want to follow the steps.

Parcel

Parcel – Blazing fast, zero configuration web application bundler

https://parceljs.org/

Mithril

Mithril is a modern client-side JavaScript framework for building Single Page Applications. It’s small (< 10kb gzip), fast and provides routing and XHR utilities out of the box.

https://mithril.js.org

Mithril will serve as our abstraction of the DOM. It can also be used to communicate with a backend, but this tutorial is frontend only. We use its hyperscript to create templates and wire them together. Anything that has a ‚view‘ function is a component. The ‚view‘ function returns a hyperscript object with can look e.g. like

m('div.wide-container#main-component', {onclick: e => startSomething()},'Hello world')

which is equivalent to

<div class="wide-container" id="main-component" click="startSomething()>Hello world</div>

in HTML. We will in addition use tagl which lets us write the above hyperscript

div.wideContainer$mainComponent({onclick: e => startSomething()},'Hello world')

what can be regarded as hyper hyper. One great benefit of Mithril is that it only contains about 10-20 functions and can be learned really quick.

Setting up the environment

Create a folder with a catchy name and enter it. I will use parcel-mithril-showcase

mkdir parcel-mithril-showcase
cd parcel-mithril-showcase
npm init -y

Let’s setup our environment. ParcelJS bundler is used to create this app. We can either install it globally, in case we want to always be able to create such prototypes fast

npm install -g parcel-bundler

or locally to the project (this can also be done at a later stage), e.g. when we work with others on this prototype and we want to spare them the setup.

npm install --save-dev parcel-bundler

Since the latter does not install the executable to the path we can add it to our package.json file like this.

"scripts": {
    "start": "parcel src/index.html",
    ...
}

This instructs parcel that the index.html is the main entry point of the application. Next we need to create this file and some more empty files to be able to start coding.

.
├── package.json
└── src
    ├── index.html
    ├── main.css
    └── main.js

The content of index.html is

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Parcel Mithril Showcase</title>
    <link rel="stylesheet" href="main.css">
</head>
<body>
    <script src="main.js"></script>
</body>
</html>

Now the development environment is started by

parcel src/index.html

or, depending on your above decision

npm start

Coding

Open the file main.js and import mithril

import m from 'mithril';

When you save and watch your shell, parcel will in the background immediately start to import mithril and automagically add it to package.json as a dependency. We do the same for tagl-mithril.

import tagl from 'tagl-mithril';

Since we use tagl now, we can define the tags used in our page as abbreviations as follows, let’s start with a fat heading <h1>

const {h1, div} = tagl(m);

Tagl will use Mithril’s hyperscript function m to provide Mithril components that can be used directly in the code. This is all that is needed to render the first content to the page with a component, that will replace the html body.

m.mount(document.body, {
    view: vnode => div.container(
        h1('Hello Mithril'),
        p('With the help of tagl'),
    )
});

Open a browser and navigate to http://localhost:1234 which is the default of parcel for the preview. You will see the „Hello world“, rendered by Mithril.

Basically this is it. You might say, well I’ve seen hello world in a gazillion programming languages before. Correct, but this is not where the fun stops.

Usually I like it not to write too much css. When I don’t want to write anything and just use bare tags, I just include mini.css for the sake of my eyes. In main.css add the line

@import 'mini.css';

…, save and parcel does the magic. The browser will update it’s contents and the page looks a bit nicer.

Creating a more fancy component

Since the creation of components is something you do very often, I created a snippet for this matter:

import m from 'mithril';
import tagl from 'tagl-mithril';

// prettier-ignore
const { address, aside, footer, header, h1, h2, h3, h4, h5, h6, hgroup, main, nav, section, article, blockquote, dd, dir, div, dl, dt, figcaption, figure, hr, li, ol, p, pre, ul, a, abbr, b, bdi, bdo, br, cite, code, data, dfn, em, i, kdm, mark, q, rb, rp, rt, rtc, ruby, s, samp, small, span, strong, sub, sup, time, tt, u, wbr, area, audio, img, map, track, video, embed, iframe, noembed, object, param, picture, source, canvas, noscript, script, del, ins, caption, col, colgroup, table, tbody, td, tfoot, th, thead, tr, button, datalist, fieldset, form, formfield, input, label, legend, meter, optgroup, option, output, progress, select, textarea, details, dialog, menu, menuitem, summary, content, element, slot, template } = tagl(m);

export default vnode => {
    return {
        view(vnode) {
            return [
                'Write your hyperscript here'
            ];
        }
    };
};

This is our starting point. The component everyone needs for his webpage is an analogue clock, because they look nice and something moves. Since Mithril can render any tag, let us try to code it in svg, which is a tag based graphics format and therefore can be directly written using the toolkit.

Stealing the design

Let us visit wikipedia and look for svg-codes for analogue clocks… Ah, here we are (Read about the license on the linked page).

<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink"
     viewBox="-1024 -1024 2048 2048" width="600" height="600">
  <title>Swiss Railway Clock</title>
  <style type="text/css">
    .bg {stroke: none; fill: white;}
    .fc {stroke: none; fill: black;}
    .h1 {stroke: none; fill: black;}
    .h2 {stroke: none; fill: #aa0000;}
  </style>
  <defs>
    <path id="mark1" d="M -20,-1000 l 40,0 0,100 -40,0 z" />
    <path id="mark2" d="M -40,-1000 l 80,0 0,240 -80,0 z" />
    <path id="mark3" d="M -40,-1000 l 80,0 0,300 -80,0 z" />
    <path id="handh" d="M -50,-600  l 50,-50 50,50 0,800  -100,0 z" />
    <path id="handm" d="M -40,-900  l 40,-40 40,40 0,1180 -80,0  z" />
    <g    id="hands">
      <path d="M -10,-910 l  10,-10 10,10 2,300 -24,0 z
               M -13,-390 l  26,0         7,690 -40,0 z" />
      <path d="M   0,-620 a 120,120 0 0 1 0,240
                          a 120,120 0 0 1 0,-240 z
               M   0,-560 a  60,60  0 0 0 0,120
                          a  60,60  0 0 0 0,-120 z" />
    </g>
    <g id="face1">
      <use xlink:href="#mark1" transform="rotate(06)" />
      <use xlink:href="#mark1" transform="rotate(12)" />
      <use xlink:href="#mark1" transform="rotate(18)" />
      <use xlink:href="#mark1" transform="rotate(24)" />
    </g>
    <g id="face2">
      <use xlink:href="#face1" />
      <use xlink:href="#face1" transform="rotate(30)" />
      <use xlink:href="#face1" transform="rotate(60)" />
      <use xlink:href="#mark3" />
      <use xlink:href="#mark2" transform="rotate(30)" />
      <use xlink:href="#mark2" transform="rotate(60)" />
    </g>
    <g id="face">
      <use xlink:href="#face2" />
      <use xlink:href="#face2" transform="rotate(90)"  />
      <use xlink:href="#face2" transform="rotate(180)" />
      <use xlink:href="#face2" transform="rotate(270)" />
    </g>
  </defs>
  <circle class="bg" r="1024" />
  <use xlink:href="#face"  class="fc" />
  <use xlink:href="#handh" class="h1" transform="rotate(304.5)" />
  <use xlink:href="#handm" class="h1" transform="rotate(54)" />
  <use xlink:href="#hands" class="h2" transform="rotate(12)" />
  <!-- hands at 10:09:02 -->
</svg>

Well either you are really good with regular expressions or you also might use the nice Mithril from HTML generator by Arthur Clemens, which directly outputs this:

m("svg", {"xmlns":"http://www.w3.org/2000/svg","xmlns:xlink":"http://www.w3.org/1999/xlink","viewBox":"-1024 -1024 2048 2048","width":"600","height":"600"},
  [
    m("title", 
      "Swiss Railway Clock"
    ),
    m("style", {"type":"text/css"}, 
      " .bg {stroke: none; fill: white;} .fc {stroke: none; fill: black;} .h1 {stroke: none; fill: black;} .h2 {stroke: none; fill: #aa0000;} "
    ),
    m("defs",
      [
        m("path", {"id":"mark1","d":"M -20,-1000 l 40,0 0,100 -40,0 z"}),
        m("path", {"id":"mark2","d":"M -40,-1000 l 80,0 0,240 -80,0 z"}),
        m("path", {"id":"mark3","d":"M -40,-1000 l 80,0 0,300 -80,0 z"}),
        m("path", {"id":"handh","d":"M -50,-600  l 50,-50 50,50 0,800  -100,0 z"}),
        m("path", {"id":"handm","d":"M -40,-900  l 40,-40 40,40 0,1180 -80,0  z"}),
        m("g", {"id":"hands"},
          [
            m("path", {"d":"M -10,-910 l  10,-10 10,10 2,300 -24,0 z\n               M -13,-390 l  26,0         7,690 -40,0 z"}),
            m("path", {"d":"M   0,-620 a 120,120 0 0 1 0,240\n                          a 120,120 0 0 1 0,-240 z\n               M   0,-560 a  60,60  0 0 0 0,120\n                          a  60,60  0 0 0 0,-120 z"})
          ]
        ),
        m("g", {"id":"face1"},
          [
            m("use", {"xlink:href":"#mark1","transform":"rotate(06)"}),
            m("use", {"xlink:href":"#mark1","transform":"rotate(12)"}),
            m("use", {"xlink:href":"#mark1","transform":"rotate(18)"}),
            m("use", {"xlink:href":"#mark1","transform":"rotate(24)"})
          ]
        ),
        m("g", {"id":"face2"},
          [
            m("use", {"xlink:href":"#face1"}),
            m("use", {"xlink:href":"#face1","transform":"rotate(30)"}),
            m("use", {"xlink:href":"#face1","transform":"rotate(60)"}),
            m("use", {"xlink:href":"#mark3"}),
            m("use", {"xlink:href":"#mark2","transform":"rotate(30)"}),
            m("use", {"xlink:href":"#mark2","transform":"rotate(60)"})
          ]
        ),
        m("g", {"id":"face"},
          [
            m("use", {"xlink:href":"#face2"}),
            m("use", {"xlink:href":"#face2","transform":"rotate(90)"}),
            m("use", {"xlink:href":"#face2","transform":"rotate(180)"}),
            m("use", {"xlink:href":"#face2","transform":"rotate(270)"})
          ]
        )
      ]
    ),
    m("circle", {"class":"bg","r":"1024"}),
    m("use", {"class":"fc","xlink:href":"#face"}),
    m("use", {"class":"h1","xlink:href":"#handh","transform":"rotate(304.5)"}),
    m("use", {"class":"h1","xlink:href":"#handm","transform":"rotate(54)"}),
    m("use", {"class":"h2","xlink:href":"#hands","transform":"rotate(12)"})
  ]
)

Now we can use a less fancy regex to convert to tagl format (which is not necessary, but I prefer it in terms of readability).

/m\("([^"]*)",/$1(

which gives us

svg( {"xmlns":"http://www.w3.org/2000/svg","xmlns:xlink":"http://www.w3.org/1999/xlink","viewBox":"-1024 -1024 2048 2048","width":"600","height":"600"},
  [
    title( 
      "Swiss Railway Clock"
    ),
    style( {"type":"text/css"}, 
      " .bg {stroke: none; fill: white;} .fc {stroke: none; fill: black;} .h1 {stroke: none; fill: black;} .h2 {stroke: none; fill: #aa0000;} "
    ),
    defs(
      [
        path( {"id":"mark1","d":"M -20,-1000 l 40,0 0,100 -40,0 z"}),
        path( {"id":"mark2","d":"M -40,-1000 l 80,0 0,240 -80,0 z"}),
        path( {"id":"mark3","d":"M -40,-1000 l 80,0 0,300 -80,0 z"}),
        path( {"id":"handh","d":"M -50,-600  l 50,-50 50,50 0,800  -100,0 z"}),
        path( {"id":"handm","d":"M -40,-900  l 40,-40 40,40 0,1180 -80,0  z"}),
        g( {"id":"hands"},
          [
            path( {"d":"M -10,-910 l  10,-10 10,10 2,300 -24,0 z\n               M -13,-390 l  26,0         7,690 -40,0 z"}),
            path( {"d":"M   0,-620 a 120,120 0 0 1 0,240\n                          a 120,120 0 0 1 0,-240 z\n               M   0,-560 a  60,60  0 0 0 0,120\n                          a  60,60  0 0 0 0,-120 z"})
          ]
        ),
        g( {"id":"face1"},
          [
            use( {"xlink:href":"#mark1","transform":"rotate(06)"}),
            use( {"xlink:href":"#mark1","transform":"rotate(12)"}),
            use( {"xlink:href":"#mark1","transform":"rotate(18)"}),
            use( {"xlink:href":"#mark1","transform":"rotate(24)"})
          ]
        ),
        g( {"id":"face2"},
          [
            use( {"xlink:href":"#face1"}),
            use( {"xlink:href":"#face1","transform":"rotate(30)"}),
            use( {"xlink:href":"#face1","transform":"rotate(60)"}),
            use( {"xlink:href":"#mark3"}),
            use( {"xlink:href":"#mark2","transform":"rotate(30)"}),
            use( {"xlink:href":"#mark2","transform":"rotate(60)"})
          ]
        ),
        g( {"id":"face"},
          [
            use( {"xlink:href":"#face2"}),
            use( {"xlink:href":"#face2","transform":"rotate(90)"}),
            use( {"xlink:href":"#face2","transform":"rotate(180)"}),
            use( {"xlink:href":"#face2","transform":"rotate(270)"})
          ]
        )
      ]
    ),
    circle( {"class":"bg","r":"1024"}),
    use( {"class":"fc","xlink:href":"#face"}),
    use( {"class":"h1","xlink:href":"#handh","transform":"rotate(304.5)"}),
    use( {"class":"h1","xlink:href":"#handm","transform":"rotate(54)"}),
    use( {"class":"h2","xlink:href":"#hands","transform":"rotate(12)"})
  ]
)

This can be copied into the snippet, where it says: ‚Write your hyperscript here‘. Save it in a file called clock.js. This code now uses some tags that are not in the default snippet, that only contains all HTML5 tags as tagls. So we need to add at the top of the file:

const { svg, circle, title, g, path, defs, use, style } = tagl(m);

Then we need to import it to our application main.js.

import clock from './clock';

And we can now use it as an own ‚tag‘ with the usual hyperscript syntax

m(clock)

after the div(), call in main.js. Next thing needed is to come up with a useful interface to the clock. Either we make it totally self-contained and it knows about the time to display on its own, or we pass in the angles of the hands. Or we let the user supply the hour from 0 to 24 and the minute from 0 to 60 and the seconds analogously. This sounds reasonable, but we could add both interfaces later on. All property key value pairs that are supplied on the JS-object that is passed as first parameter to a tagl can be used inside a component by querying its vnode.attrs. So we read in clock.js all attribute values like this

export default vnode => {
    return {
        view(vnode) {
            const {hour, minute, second} = vnode.attrs;
            return [
...

and use them to replace the predefined wikipedia angles of hour=304.5 minute=54 and second=12 by replacing the lines with:

use( {"class":"h1","xlink:href":"#handh","transform":"rotate(${hour*360/12})`)"}),
use( {"class":"h1","xlink:href":"#handm","transform":"rotate(${minute*360/60})`)"}),
use( {"class":"h2","xlink:href":"#hands","transform":"rotate(${second*360/60})`)"})

Now back in main.js we can supply the clock with the values of the current time

view: vnode => {
    const time = new Date()
    const hour = time.getHours();
    const minute = time.getMinutes();
    const second = time.getSeconds();
...
    m(clock, {hour, minute, second})

Now it looks as if the story ends here, but when you look in the browser, you might notice that the clock doesn’t move. Yes and that’s part of the supreme performance of Mithril. It knows exactly when to update the view and when not. Usually nothing will update automatically except when user events such as clicks or other subscribed events are triggered. Some other types of events that are not temporarily directly coupled to the users doings need a manual trigger, such as finished HTTP requests or a clock that wants to be updated. We can place the following line anywhere in main.js:

setInterval(m.redraw,1000)

Now the story could be over, but of course the clock doesn’t behave very natural, because the hour hand will remain on exactly the 11th hour mark from 23:00-24:00 and then move from 330° to 0° in one instant. Using this last code in the main.js file, we are now set with the clock.

import m from 'mithril';
import tagl from 'tagl-mithril';
import clock from './clock';

const { h1, div, p } = tagl(m);

setInterval(m.redraw, 100)

m.mount(document.body, {
    view: vnode => {
        const time = new Date();
        const hour = time.getHours() + time.getMinutes() / 60;
        const minute = time.getMinutes() + time.getSeconds() / 60;
        const second = time.getSeconds() + time.getMilliseconds() / 1000;
        return div.container(
            h1('Hello Mithril'),
            p('With the help of tagl'),
            m(clock, { hour, minute, second, size: 120})
        );
    }
});

This last modification concludes the tutorial. More might follow, but why? This is it, except for:

Deployment

Now there is the second time parcel messes up with the tutorial.

parcel build src/index.html

You can also bundle it all together in one html file, as I did here (Yes, it IS the 404 page that I mean! :). Behold the final result:

Homework 1

You might have noticed the size parameter in

        m(clock, { hour, minute, second, size: 120})

Make the clock’s size configurable.

Homework 2

Disclaimer

Not all of the above are best Mithril practices. E.g. the calculation of the clock angles in the view function is not regarded optimal.

Tetras

Code https://github.com/abulvenz/tetras

Live https://abulvenz.github.io/tetras