Adding SQL localization data using an Angular form and ASP.NET Core

This article shows how SQL localized data can be added to a database using Angular forms which can then be displayed without restarting the application. The ASP.NET Core localization is implemented using Localization.SqlLocalizer. This NuGet package is used to save and retrieve the dynamic localized data. This makes it possible to add localized data at run-time.

Code: https://github.com/damienbod/AngularLocalizationAspNetCore

2018-08-01: ASP.NET Core 2.1, Localization.SqlLocalizer 2.0.2 , Angular 6.1.0, angular-l10n 5.0.0
2017-08-20: ASP.NET Core 2.0, Localization.SqlLocalizer 2.0.0 , Angular 4.3.5, angular-l10n 3.5.0
2017-05-14: Localization.SqlLocalizer 1.0.10 , angular 4.1.0, latest dotnet core packages
2017-02-03: Updated to angular-l10n 2.0.0, VS2017 RC3 msbuild3, angular 2.4.5 and webpack 2.2.1
2017-01-07: Updated to ASP.NET Core 1.1 and VS2017 with csproj, angular 2.4.1 and webpack 2.2.0-rc.3
2016-11-01: Updated to Angular 2.1.1, angular2localization 1.1.0, Webpack 2, AoT, treeshaking
2016-09-18: Updated to Angular2 release and ASP.NET Core 1.0.1
2016-08-14: Updated to Angular2 rc.6 and angular2localization 0.10.0
2016-06-28: Updated to Angular2 rc.3, angular2localization 0.8.5 and dotnet RTM

Posts in this series

The ASP.NET Core API provides an HTTP POST action method which allows the user to add a new ProductCreateEditDto object to the application. The view model adds both product data and also localization data to the SQLite database using Entity Framework Core.

[HttpPost]
public IActionResult Post([FromBody]ProductCreateEditDto value)
{
	_productCudProvider.AddProduct(value);
	return Created("http://localhost:5000/api/ShopAdmin/", value);
}

The Angular app uses the ProductService to send the HTTP POST request to the ShopAdmin service. The post methods sends the payload as a json object in the body of the request.

public CreateProduct = (product: ProductCreateEdit): Observable<ProductCreateEdit> => {
	let item: string = JSON.stringify(product);
	this.setHeaders();
	return this._http.post(this.actionUrlShopAdmin, item, {
		headers: this.headers,
                body: '',
	}).map((response: Response) => <ProductCreateEdit>response.json())
	.catch(this.handleError);
}

The client model is the same as the server side view model. The ProductCreateEdit class has an array of localized records.

import { LocalizationRecord } from './LocalizationRecord';

export class ProductCreateEdit {
    Id: number;
    Name: string;
    Description: string;
    ImagePath: string;
    PriceEUR: number;
    PriceCHF: number;
    LocalizationRecords: LocalizationRecord[];
} 

export class LocalizationRecord {
    Key: string;
    Text: string;
    LocalizationCulture: string;
} 

The shop-admin.component.html template contains the form which is used to enter the data and this is then sent to the server using the product service. The forms in Angular 2 have changed a lot compared to Angular 1 forms. The form uses the FormModule to define the Angular 2 form specifics. These control items need to be defined in the corresponding ts file.

<div class="container">
    <div [hidden]="submitted">
        <h1>New Product</h1>
        <form *ngIf="active" (ngSubmit)="onSubmit()" #productForm="ngForm">
            <div class="form-group">
                <label class="control-label" for="name">{{ 'ADD_PRODUCT_NAME' | translate:lang }}</label>
                <input type="text" class="form-control" id="name" required  [(ngModel)]="Product.Name" name="name" placeholder="name" #name="ngModel">
                <div [hidden]="name.valid || name.pristine" class="alert alert-danger">
                    Name is required
                </div>
            </div>
          
            <div class="form-group">
                <label class="control-label" for="description">{{ 'ADD_PRODUCT_DESCRIPTION' | translate:lang }}</label>
                <input type="text" class="form-control" id="description" required [(ngModel)]="Product.Description" name="description" placeholder="description" #description="ngModel">
                <div [hidden]="description.valid || description.pristine" class="alert alert-danger">
                    description is required
                </div>
            </div>

            <div class="form-group">
                <label class="control-label" for="priceEUR">{{ 'ADD_PRODUCT_PRICE_EUR' | translate:lang }}</label>
                <input type="number" class="form-control" id="priceEUR" required [(ngModel)]="Product.PriceEUR" name="priceEUR" placeholder="priceEUR" #priceEUR="ngModel">
                <div [hidden]="priceEUR.valid || priceEUR.pristine" class="alert alert-danger">
                    priceEUR is required
                </div>
            </div>

            <div class="form-group">
                <label class="control-label" for="priceCHF">{{ 'ADD_PRODUCT_PRICE_CHF' | translate:lang }}</label>
                <input type="number" class="form-control" id="priceCHF" required [(ngModel)]="Product.PriceCHF" name="priceCHF" placeholder="priceCHF" #priceCHF="ngModel">
                <div [hidden]="priceCHF.valid || priceCHF.pristine" class="alert alert-danger">
                    priceCHF is required
                </div>
            </div>

            <div class="form-group">
                <label class="control-label" >{{ 'ADD_PRODUCT_LOCALIZED_NAME' | translate:lang }}</label>

                <div class="row">
                    <div class="col-md-3"><em>de</em></div>
                    <div class="col-md-9">
                        <input type="text" class="form-control" id="Namede" required [(ngModel)]="Name_de"  name="Namede" #Namede="ngModel">
                        <div [hidden]="Namede.valid || Namede.pristine" class="alert alert-danger">
                            Name_de is required
                        </div>
                    </div>
                </div>
                <div class="row">
                    <div class="col-md-3"><em>fr</em></div>
                    <div class="col-md-9">
                        <input type="text" class="form-control" id="Namefr" required  [(ngModel)]="Name_fr"  name="Namefr" #Namefr="ngModel">
                        <div [hidden]="Namefr.valid || Namefr.pristine" class="alert alert-danger">
                            Name_fr is required
                        </div>
                    </div>
                </div>
                <div class="row">
                    <div class="col-md-3"><em>it</em></div>
                    <div class="col-md-9">
                        <input type="text" class="form-control" id="Nameit" required [(ngModel)]="Name_it" name="Nameit" #Nameit="ngModel">
                        <div [hidden]="Nameit.valid || Nameit.pristine" class="alert alert-danger">
                            Name_it is required
                        </div>
                    </div>
                </div>
                <div class="row">
                    <div class="col-md-3"><em>en</em></div>
                    <div class="col-md-9">
                        <input type="text" class="form-control" id="Nameen" required [(ngModel)]="Name_en"  name="Nameen" #Nameen="ngModel">
                        <div [hidden]="Nameen.valid || Nameen.pristine" class="alert alert-danger">
                            Name_en is required
                        </div>
                    </div>
                </div>

            </div>

            <div class="form-group">
                <label class="control-label">{{ 'ADD_PRODUCT_LOCALIZED_DESCRIPTION' | translate:lang }}</label>

                <div class="row">
                    <div class="col-md-3"><em>de</em></div>
                    <div class="col-md-9">
                        <input type="text" class="form-control" id="Descriptionde" required [(ngModel)]="Description_de" name="Descriptionde" #Descriptionde="ngModel">
                        <div [hidden]="Descriptionde.valid || Descriptionde.pristine" class="alert alert-danger">
                            Description DE is required
                        </div>
                    </div>
                </div>
                <div class="row">
                    <div class="col-md-3"><em>fr</em></div>
                    <div class="col-md-9">
                        <input type="text" class="form-control" id="Descriptionfr" required [(ngModel)]="Description_fr" name="Descriptionfr" #Descriptionfr="ngModel">
                        <div [hidden]="Descriptionfr.valid || Descriptionfr.pristine" class="alert alert-danger">
                            Description FR is required
                        </div>
                    </div>
                </div>
                <div class="row">
                    <div class="col-md-3"><em>it</em></div>
                    <div class="col-md-9">
                        <input type="text" class="form-control" id="Descriptionit" required [(ngModel)]="Description_it" name="Descriptionit" #Descriptionit="ngModel">
                        <div [hidden]="Descriptionit.valid || Descriptionit.pristine" class="alert alert-danger">
                            Description IT is required
                        </div>
                    </div>
                </div>
                <div class="row">
                    <div class="col-md-3"><em>en</em></div>
                    <div class="col-md-9">
                        <input type="text" class="form-control" id="Descriptionen" required [(ngModel)]="Description_en" name="Descriptionen" #Descriptionen="ngModel">
                        <div [hidden]="Descriptionen.valid || Descriptionen.pristine" class="alert alert-danger">
                            Description EN is required
                        </div>
                    </div>
                </div>

            </div>
            <button type="submit" class="btn btn-default" [disabled]="!productForm.form.valid">Submit</button>

        </form>
    </div>
</div>

The built in Angular form components are imported from the ‘@angular/common’ library. The different Validators, or your own custom Validators can be added here. The Create method uses the Control items and the Product model to create the full product item and send the data to the Shop Admin Controller on the server. When successfully created, the user is redirected to the Shop component showing all products in the selected language.

import { Component, OnInit } from '@angular/core';
import { Router} from '@angular/router';
import { Product } from '../services/Product';
import { ProductCreateEdit } from  '../services/ProductCreateEdit';
import { Localization, LocaleService, TranslationService } from 'angular-l10n';
import { ProductService } from '../services/ProductService';

@Component({
    selector: 'shop-admincomponent',
    templateUrl: 'shop-admin.component.html'
})

export class ShopAdminComponent extends Localization implements OnInit  {

    public message: string;
    public Product: ProductCreateEdit = new ProductCreateEdit();
    public Currency: string;

    public Name_de: string;
    public Name_fr: string;
    public Name_it: string;
    public Name_en: string;
    public Description_de: string;
    public Description_fr: string;
    public Description_it: string;
    public Description_en: string;

    submitted = false;
    // Reset the form with a new hero AND restore 'pristine' class state
    // by toggling 'active' flag which causes the form
    // to be removed/re-added in a tick via NgIf
    // TODO: Workaround until NgForm has a reset method (#6822)
    active = true;
    saving = false;

    constructor(
        private router: Router,
        public locale: LocaleService,
        public translation: TranslationService,
        private _productService: ProductService
    ) {
        super(locale, translation);

        this.message = 'shop-admin.component';

        this.locale.defaultLocaleChanged.subscribe(
            (item: string) => { this.onLanguageCodeChangedDataRecieved(item); }
        );
    }

    onSubmit() {
        this.submitted = true;
        this.Create();
    }

    ngOnInit() {
        console.log('ngOnInit ShopAdminComponent');
        // TODO Get product if Id exists
        this.initProduct();

        this.Currency = this.locale.getCurrentCurrency();
        if (!(this.Currency === 'CHF' || this.Currency === 'EUR')) {
            this.Currency = 'CHF';
        }
    }

    public Create() {

        this.submitted = true;

        this.saving = true;

        this.Product.LocalizationRecords = [];
        this.Product.LocalizationRecords.push({ Key: this.Product.Name, LocalizationCulture: 'de-CH', Text: this.Name_de });
        this.Product.LocalizationRecords.push({ Key: this.Product.Name, LocalizationCulture: 'fr-CH', Text: this.Name_fr });
        this.Product.LocalizationRecords.push({ Key: this.Product.Name, LocalizationCulture: 'it-CH', Text: this.Name_it });
        this.Product.LocalizationRecords.push({ Key: this.Product.Name, LocalizationCulture: 'en-US', Text: this.Name_en });

        this.Product.LocalizationRecords.push({ Key: this.Product.Description, LocalizationCulture: 'de-CH', Text: this.Description_de });
        this.Product.LocalizationRecords.push({ Key: this.Product.Description, LocalizationCulture: 'fr-CH', Text: this.Description_fr });
        this.Product.LocalizationRecords.push({ Key: this.Product.Description, LocalizationCulture: 'it-CH', Text: this.Description_it });
        this.Product.LocalizationRecords.push({ Key: this.Product.Description, LocalizationCulture: 'en-US', Text: this.Description_en });

        this._productService.CreateProduct(this.Product)
            .subscribe(data => {
                this.saving = false;
                this.router.navigate(['/shop']);
            }, error => {
                this.saving = false;
                console.log(error);
            },
            () => this.saving = false);
    }

    private onLanguageCodeChangedDataRecieved(item: string) {
        console.log('onLanguageCodeChangedDataRecieved Shop Admin');
        console.log(item + ' : ' + this.locale.getCurrentLanguage());
    }

    private initProduct() {
        this.Product = new ProductCreateEdit();
    }
}

The form can then be used and the data is sent to the server.
localizedAngular2Form_01

And then displayed in the Shop component.

localizedAngular2Form_02

Notes

Angular forms have a few validation issues which makes me uncomfortable using it.

Links

https://angular.io/docs/ts/latest/guide/forms.html

https://auth0.com/blog/2016/05/03/angular2-series-forms-and-custom-validation/

http://odetocode.com/blogs/scott/archive/2016/05/02/the-good-and-the-bad-of-programming-forms-in-angular.aspx

http://blog.thoughtram.io/angular/2016/03/14/custom-validators-in-angular-2.html

http://restlet.com/blog/2016/02/11/implementing-angular2-forms-beyond-basics-part-1/

https://docs.asp.net/en/latest/fundamentals/localization.html

https://www.nuget.org/profiles/damienbod

https://github.com/robisim74/angular2localization

https://angular.io

https://docs.asp.net/en/latest/fundamentals/localization.html

3 comments

  1. […] Adding SQL localization data using an Angular 2 form and ASP.NET Core – Damian Bowden looks at managing Localization data in SQL databases using a front end in Angular and having the localizations take action without application restarts. […]

  2. […] the full article, click here. @jeremylikness: “Adding #SQL localization using #dotnet Core with an #Angular2 form […]

  3. ovisariesdk · · Reply

    The migrations aren’t working.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.