Part IV - Applying profiles

In this part, you will learn how to extend your model with custom meta data and use this meta data to enrich your code generation templates. As already noted in part III, some generated UI elements don't seem finished yet: description fields have a single line text input, and UI labels use technical attribute names for their text.

At the end of this part, you have learned how to:

  • Generate custom UI labels for navigation, tables and forms.
  • Generate customized HTML inputs for forms.

... all using a simple and type-safe (!) extension mechanism.

Extending the bookstore model

First, you need to extend the model with a profile. If you never heard of profiles before: a profile is a UML extension mechamism, which is particularly powerful when used for code generation. This is the official UML definition:

"A profile defines limited extensions to a reference metamodel with the purpose of adapting the metamodel to a specific platform or domain."

In this part of the tutorial, you will see how a profile may look like and how you can apply it to a model. In short, a profile is a special kind of model package, which can have multipe stereotypes. A stereotype, in turn, describes how elements in a model (such as classes and properties) can be extended with extra values. These extra values are called tagged values. This is really all you need to know about profiles for now.

Open the bookstore model (CodeGen/bookstore.ymn) in Yellicode Modeler. A profile has already been prepared for you, so you don't need to create one from scratch. In the project explorer on the left, expand the Profiles panel. Then expand the profile named Bookstore.

The Bookstore profile has the following elements:

  • An enumeration named DataType. You will need this enumeration for customizing form input elements.
  • A stereotype named DataField. Used to control what type of HTML element is generated. It has one tag definition named DataType that uses the DataType described in the first bullet.
  • A stereotype named LabeledElement. Used to control the generated text for UI labels.

Just inspect the profile, leave everything as-is for now and apply it to the model:

  1. In the Model tab of the project explorer, right click on the Bookstore package and select Apply Profile(s).
  2. Check Bookstore. This makes the stereotypes from our Bookstore profile available to the model.
  3. Open the Book class and, in the General section, check LabeledElement. A Tagged Values section will appear.
  4. Enter a plural (Books) and singular (Book) text for Book.
  5. Repeat the steps 3-4 for the other classes.

When done, save your changes to the model file (CTRL+S).

Applying plural names to the side navigation

Now let's first fix the side navigation of the Bookstore app, which still contains singular names. Open template file CodeGen/app/side-nav.template.ts and add the following line to the imports:

import * as bookstore from '../bookstore';    

Then, replace the contents of the function Generator.generateFromModel(...) with the following code:

// Generate the HTML
Generator.generateFromModel(htmlTemplateOptions, (textWriter: TextWriter, model: elements.Model) => {    
    const writer = new HtmlWriter(textWriter);
    writer.writeElement('nav', {classNames: 'col-sm-3 col-md-2 d-none d-sm-block bg-light sidebar'}, () => {
        writer.writeElement('ul', {classNames: 'nav nav-pills flex-column'}, () => {
            model.getAllClasses().forEach(c => {                
                const label = bookstore.isLabeledElement(c) ? c.LabelPlural : c.name; 
                writer.writeElement('li', {classNames: 'nav-item'}, () => {
                    writer.writeElement('a', { 
                        classNames: 'nav-link', 
                        attributes: {href: '#', routerLink: PathUtility.createPathSegment(c)}},
                        label);
                })    
            });            
        });
    });  
});

Notice the following line:

const label = bookstore.isLabeledElement(c) ? c.LabelPlural : c.name; 

In this line, you will recognise the name of a stereotype (LabeledElement) and one of it's tagged values (LabelPlural). The code asserts that the stereotype is applied to the class, and if so, gets the configured plural label. If not, it falls back to the name of the class.

Now you probably wonder where the bookstore module name comes from. Well, when you save a model file that has a profile in it, Yellicode also generates a strongly-typed TypeScript API for that! The API is a normal .ts file, and can be found in the same folder as the model file.

Try changing the name of the 'LabelPlural' tag definition in the Modeler to something else and then saving your changes. VS Code will detect this change automatically and will warn you about a TypeScript error until you fixed the name in the template.

Now that you have updated the template, let's run it. Make sure that the template is saved and the Yellicode CLI is running:

yellicode --watch

Now start both the API (CTRL+F5) and the App (enter npm start in the terminal) and notice the updated titles in the side navigation.

Made a spelling mistake in one of the plural names? Correct it in the model and save your changes. Then just watch how your browser refreshes and shows the updated app!

Dealing with different input types

In this step, you will learn how to customize the control types of your form inputs. Open file CodeGen/app/form.template.ts and add the following line to the imports:

import * as bookstore from '../bookstore';    

Then replace the formGroupTemplate() function with the following code:

const formGroupTemplate = (writer: HtmlWriter, att: elements.Property) => {
    const htmlInputId = NameUtility.camelToKebabCase(att.name);
    const isRequired = att.isRequiredAndSinglevalued();

    // Common attributes
    const htmlAttributes = {id: htmlInputId, name: att.name,  '[(ngModel)]': `model.${att.name}`, required: isRequired};

    // Label     
    writer.writeElement('label', { attributes: { for: htmlInputId } }, bookstore.isLabeledElement(att) ? att.LabelSingular : att.name);

    // Input
    if (!elements.isClass(att.type)) {        
        let htmlInputType: string;
        if (bookstore.isDataField(att) && att.DataType) {
            htmlInputType = att.DataType;
        }
        else if (elements.isPrimitiveBoolean(att.type)) {
            htmlInputType = 'checkbox';
        }
        else if (elements.isPrimitiveInteger(att.type)) {
            htmlInputType = 'number';
        }
        else htmlInputType = 'text';
        if (htmlInputType === bookstore.DataType.TextMultiLine) {
            // Exception for TextMultiLine: we need a textarea instead of a input element
            htmlAttributes['rows'] = 8;
            writer.writeElement('textarea', { classNames: 'form-control', attributes: htmlAttributes });
        }
        else {
            htmlAttributes['type'] = htmlInputType;
            writer.writeElement('input', { classNames: 'form-control', attributes: htmlAttributes });
        }
    }
    else {       
        htmlAttributes['[compareWith]'] = 'compareOptions'; 
        htmlAttributes['multiple'] = att.isMultivalued(); 
        writer.writeElement('select', { classNames: 'form-control', attributes: htmlAttributes }, () => {
            writer.writeElement('option', { attributes: { '*ngFor': `let opt of ${att.name}Options`, '[ngValue]': 'opt' } }, '{{opt.displayName}}');
        });
    }
}

This code checks if an attribute has the DataField stereotype applied, and if so, uses the value of the DataType tagged value as the type for the HTML input element. This works because the DataType enumeration is a string enumeration, of which the values match those of the the allowed "type" attributes of the <input> element. You need to make one exception for multline-text though, because you cannot use an <input> element for that.

Now, extend the model by taking the following steps.

  1. Open the Author class in the Modeler and open the About attribute.
  2. In the attribute properties, check the checkbox for the DataField stereotype. A Tagged Values section appears.
  3. In the Tagged Values section, open the DataType dropdown and select TextMultiLine.
  4. Repeat steps 1-3 for the Description attribute of the Category class.

When you are done, save the model.

A few more labels

You are almost done. You may have noticed in the previous step that the code already updated the label for the form input. As a final step, you need to do the same for the index pages. Open file CodeGen/app/table.template.ts and add the following line to the imports:

import * as bookstore from '../bookstore';    

Then, find the following line:

writer.writeElement('th', {classNames: NameUtility.camelToKebabCase(att.name)}, `${att.name}`);    
And replace it with the following:
const label = (bookstore.isLabeledElement(att) && att.LabelSingular) ? 
                att.LabelSingular : // use the label from the bookstore model, 
                NameUtility.lowerToUpperCamelCase(att.name); // and fallback to the name of the attribute
writer.writeElement('th', {classNames: NameUtility.camelToKebabCase(att.name)}, label);

Of course, to let this code take effect, you also need to enter a singular label for some attributes. We will leave that as an exercise for the reader.

Generating the final UI

Now, update the front-end code by restarting the Yellicode CLI.

  1. Open the terminal where you started the CLI, press CTRL+C.
  2. Confirm with 'Y'.
  3. Start the CLI again:
    yellicode --watch

Why restart? Because the CLI only watches template files that are configured in the codegenconfig.json file. But since you updated some files that are dependencies of some of these templates, changes to those will not be detected. Restarting the CLI will make sure that all main templates and their dependencies are recompiled.

If your app is still running, your browser will refresh again. Now open the Author- and Category screen and notice the new multline text fields!

Conclusion

In this last part, you've only seen two aspects that can be controlled with profiles, but it's easy to imagine more applications:

  • You might not always want to show all fields on the index- and details pages. What about defining a tagged value that hows (or hides) certain attributes?
  • Adding validation: extend the model with validation rules and generate both client and server validation from these.
  • Controlling sorting: the current application returns data in an undefined order. What about defining a tagged value for attribute sort priority and apply this in your backend?

With its strong focus on extensibility and a simplicity, Yellicode enables you to create solid, maintainable applications within a very short amount of time. Just have a look at the generated code and imagine how much time it would take you to write it all by hand... Just play around with the model (or create your own!) and see how it takes effect.

Thanks for taking the time to read through this tutorial. We hope that it will be a great starting point for your next project, and that it will inspire the community to create and share new code generators for other technologies as well.

We're looking forward to seeing what you create!