Hello Scout JS

Introduction

In this tutorial we will create your first Scout JS application.

If you don’t know what Scout JS is yet, please read the Get Started Guide first.

The application will simply show a text field and a button. Once the user enters some text and presses the button, the application displays a message box including that text.

The application does not require Maven or Java, only Node.js. Also, the tutorial does not require any specific IDE.

The goal of this tutorial is to provide a first impression of the Scout JS framework. We will start by getting the application running and then take a look at the code.

Prerequisites

Make sure you have Node.js 20 installed.

Get the Code

Clone the helloscoutjs repository and checkout the branch releases/24.2. Alternatively, you can also download and extract a ZIP file of the repository.

After that, the file and folder structure of your local copy should look like this (apart from a few more files):

Listing 1. Files and folders of the application
.
│   package.json
│   tsconfig.json
│   webpack.config.js
│
├───res
│       index.html
│
└───src
    │   helloworld.ts
    │   helloworld.less
    │
    ├───desktop
    │       Desktop.ts
    │       DesktopModel.ts
    │
    └───greeting
            HelloForm.ts
            HelloFormModel.ts

Build the Application

In the main folder, where the file package.json is located, open a terminal and execute npm install --ignore-scripts.

This creates a folder node_modules, containing all (direct and transitive) dependencies, as well as a file package-lock.json, listing all the specific versions of these dependencies.

If the dependencies defined in package.json change, run npm install again to update the node_modules folder.

Now execute npm run build:dev. This creates a dist folder that contains the transpiled and bundled files to be served to the browser.

Use npm run build:dev:watch to have these files automatically updated when the corresponding source files change.

Run the Application

Use the same or start a new terminal in the main folder and execute npm run serve.

This starts a little development server and opens the URL http://127.0.0.1:8080/ in your default browser. The server has live reload capability, that is, as soon as files in the dist folder change, the browser tab will reload automatically.

Type some text in the field and press the button to test the application. Also check out how the layout changes when you narrow the browser window (or e.g. use Google Chrome’s DevTools to emulate a smaller device).

Understand the Code

Let’s now have a closer look at the files that were needed to build this application.

In the main folder there are files containing information for the build, e.g. dependencies and entry points. In the subfolder res/ there are static resources that are just copied to dist/ in the build. And in the subfolder src/ you find the source files that are transformed and bundled by webpack.

Build Information

npm

For npm commands like npm install or npm run <script>, the file package.json provides the necessary information.

Listing 2. package.json
{
  "scripts": {
    "build:dev": "scout-scripts build:dev",
    "build:dev:watch": "scout-scripts build:dev:watch",
    "serve": "live-server --mount=/:dist"
  },
  "devDependencies": {
    "@eclipse-scout/cli": "^24.1.3",
    "@eclipse-scout/tsconfig": "^24.1.3",
    "live-server": "^1.2.2"
  },
  "dependencies": {
    "@eclipse-scout/core": "^24.1.3"
  }
}

The scripts define what npm run should execute. They work a bit like aliases in Bash. To have all required files available at http://127.0.0.1:8080/, we need to mount the folder dist to the root path / when starting the development server.

Modules defined in devDependencies and dependencies are downloaded to the node_modules folder on npm install. The dependency versions are prefixed with a ^ (caret), which means compatible version. That is, when running npm install, the newest version with the same major-level will be downloaded, unless another compatible version already exists in the node_modules folder or is already defined in the package-lock.json file.

For more detailed and general information about package.json and package-lock.json, see the official documentation on Node.js: The package.json guide and The package-lock.json file.

tsconfig

The tsconfig.json file configures the TypeScript compiler. It extends a default config from Scout and specifies output dir and the directories that contain TypeScript files. For more details on tsconfig see the TypeScript documentation.

webpack

As defined in package.json, the script build:dev executes scout-scripts build:dev. scout-scripts is a command provided by the @eclipse-scout/cli module. With the build:dev argument, this command uses webpack to transform and bundle the source files and write the results to the dist folder.

Scout provides a default webpack configuration which we use and adjust as follows.

Listing 3. webpack.config.js
const baseConfig = require('@eclipse-scout/cli/scripts/webpack-defaults');
module.exports = (env, args) => {
  args.resDirArray = [
    './res',
    './node_modules/@eclipse-scout/core/res',
    './node_modules/@eclipse-scout/core/dist/locales.json',
    './node_modules/@eclipse-scout/core/dist/texts.json'
  ];
  const config = baseConfig(env, args);
  config.entry = {
    'helloworld': './src/helloworld.ts',
    'helloworld-theme': './src/helloworld.less'
  };
  return config;
};

The args.resDirArray defines the files or folders with static resources to be copied to dist. In addition to the static resources of our application, we also need Scout’s static resources in node_modules/@eclipse-scout/core/.

In config.entry, the entry points for bundling JavaScript and CSS files are defined. For our application, the target files helloworld.js and helloworld-theme.css (defined without the file extension) are generated from the source files src/helloworld.ts and src/helloworld.less, respectively.

The -theme suffix of the target CSS file is important for Scout’s post-processing to work properly. Also, make sure that you don’t use exactly the same name as for the target JS file. Other than that, you can name the target files whatever you want, just make sure you also adjust the references in index.html accordingly (see next section).

For more details on the build, see Build Stack.

Static Resources

For an HTML file to be valid (see The W3C Markup Validation Service), it has to define a DOCTYPE, a default language and a title. Furthermore, to allow for responsive web design, we include the <meta> viewport element.

Listing 4. res/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Hello World</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="helloworld-theme.css" />
  </head>
  <body>
    <div class="scout"></div>
    <script src="vendors~helloworld.js"></script>
    <script src="helloworld.js"></script>
  </body>
</html>

The <link> and <script> elements include the CSS and JavaScript files generated by the build. The order of these elements is important. In particular, the <div> element with the class "scout" has to be placed before the inclusion of the scripts, since it is used to build the final DOM for our application.

Source Files

Entry Points

Listing 5. src/helloworld.ts
import {App, ObjectFactory, scout} from '@eclipse-scout/core';
import {Desktop} from './desktop/Desktop'
import {HelloForm} from './greeting/HelloForm'

scout.addObjectFactories({
  'Desktop': () => new Desktop()
});

ObjectFactory.get().registerNamespace('helloworld', {
  HelloForm
});

new App().init({
  bootstrap: {
    textsUrl: 'texts.json',
    localesUrl: 'locales.json'
  }
});

In our main TypeScript file, we import the scout namespace object as well as the class App.

Before we initialize an instance of the Scout application (passing the location of the text urls and the locales url) we do two other things:

  1. Use scout.addObjectFactories to register a function (identified by 'Desktop') that provides an instance of our Desktop class. The desktop is the main widget of a Scout application and the root of every other widget. On application initialization, Scout is using that factory to create the desktop of our application.

  2. Define our own namespace object, helloworld, and put our HelloForm class in it, so Scout can use it to build modular widgets at runtime (see DesktopModel.ts).

Listing 6. src/helloworld.less
@import "~@eclipse-scout/core/src/index";

Since we don’t need any custom styling for our application, we just import Scout’s LESS module as is in our LESS file.

To try out Scout’s dark theme, just import index-dark instead of index.

Widgets

We follow the best practice of separating model (layout, structure) and behavior code. This also makes it easier to e.g. reuse a form that should look similar elsewhere but behave differently.

A typical model definition for a Scout widget defines an objectType. This is specified as a reference to the corresponding class.

Other object properties are used to configure the widget based on the specified objectType.

Listing 7. src/greeting/DesktopModel.ts
import {Desktop, DesktopModel} from "@eclipse-scout/core";
import {HelloForm} from "../greeting/HelloForm";

export default (): DesktopModel => ({
  objectType: Desktop,
  navigationHandleVisible: false,
  navigationVisible: false,
  headerVisible: false,
  views: [
    {
      objectType: HelloForm
    }
  ]
});

The default desktop consists of a navigation, a header and a bench. We only need the bench for our application, so we hide the other parts, including the handle to toggle the navigation.

A desktop can contain outlines and/or views. We provide an instance of our HelloForm as a view on our desktop.

Listing 8. src/greeting/Desktop.ts
import {Desktop as ScoutDesktop, DesktopModel as ScoutDesktopModel} from '@eclipse-scout/core';
import DesktopModel from './DesktopModel';

export class Desktop extends ScoutDesktop {
  protected override _jsonModel(): ScoutDesktopModel {
    return DesktopModel();
  }
}

Our desktop doesn’t have any custom behavior, so we only import the DesktopModel here, in the _jsonModel() function.

Listing 9. src/greeting/HelloFormModel.ts
import {Button, Form, FormModel, GroupBox, StringField} from '@eclipse-scout/core';

export default (): FormModel => ({
  objectType: Form,
  displayHint: Form.DisplayHint.VIEW,
  modal: false,
  rootGroupBox: {
    objectType: GroupBox,
    borderDecoration: GroupBox.BorderDecoration.EMPTY,
    fields: [
      {
        id: 'NameField',
        objectType: StringField,
        label: 'Name'
      },
      {
        id: 'GreetButton',
        objectType: Button,
        label: 'Say Hello',
        keyStroke: 'enter',
        processButton: false
      }
    ]
  }
})

/* **************************************************************************
* GENERATED WIDGET MAPS
* **************************************************************************/

export type HelloFormWidgetMap = {
  'NameField': StringField;
  'GreetButton': Button;
};

Our form is defined to be non-modal and displayed as a view (rather than a dialog). It consists of a string field and a button. These are in a group box inside the form. We define an empty border decoration around this group box to have a little padding.

The Enter key is defined as the keyboard shortcut for our button, and we set processButton: false to place the button next to our field instead of above it.

At the bottom an auto generated type holding the WidgetMap can be found. Such a WidgetMap specifies which field id points to which type of widget as a flattened type. This is used later on in the Form when accessing such a widget using the this.widget() function to directly return the correct widget data type.

Listing 10. src/greeting/HelloForm.ts
import {Form, FormModel, InitModelOf, MessageBoxes} from '@eclipse-scout/core';
import HelloFormModel, {HelloFormWidgetMap} from './HelloFormModel';

export class HelloForm extends Form {

  declare widgetMap: HelloFormWidgetMap;

  protected override _jsonModel(): FormModel {
    return HelloFormModel();
  }

  protected override _init(model: InitModelOf<this>) {
    super._init(model);
    this.widget('GreetButton').on('click', () => {
      let name = this.widget('NameField').value || 'stranger';
      MessageBoxes.openOk(this.session.desktop, `Hello ${name}!`);
    });
  }
}

As in Desktop.ts, we import the model but additionally add an event handler in the _init(model) function to implement the desired behavior when the button is clicked.

To accomplish this, we can access our button and field by their respective id (see HelloFormModel.ts). An OK message box with the desired text is displayed using the convenience class MessageBoxes from Scout.

Git configuration

If you want to add the created application to a Git repository, it is recommended to exclude some files from the SCM.

As a starting point, use the file you cloned/downloaded from the helloscoutjs repository.

Listing 11. .gitignore
# Git
*.orig

# Node
node_modules/
dist/
test-results/
package-lock.json

# Do not check in any log files
*.log

# IDEs
.idea

See the gitignore Documentation for details.

What’s Next?

Now that you have successfully created your first Scout JS application, you might want to learn more about Scout JS.

If you are interested in adding a REST backend you should have a look at Hello Scout JS Full Stack Tutorial.

To see more example code of Scout JS, we recommend looking at the Scout JS Widgets application and its source code.

If you are interested in Scout’s concepts, architecture and features you probably want to have a look at the Technical Guide.

In case you should get stuck somewhere and need help, contact us on the Scout Forum or on Stack Overflow.

We wish you all the best on your journey with Scout.