Using RxJS to implement a simple ELM architecture application

Source: Internet
Author: User
Tags export class

Using RxJS to implement a simple ELM architecture application

Label (Space delimited): Front end

What is the ELM architecture

The ELM architecture is a simple architecture that uses the ELM language to write Web front-end applications and has a good advantage in code modularity, code reuse, and testing. With the ELM architecture, complex WEB applications can be built very easily, both in the face of refactoring or adding new features, to keep your project in a good state of health.

The application of the ELM architecture is usually made up of three parts- model , update , view . The three use a Message to communicate with each other.

Model

A model is usually a simple POJO object that contains the data that needs to be presented or the state information of the interface display logic, in the Elm language, which is usually a custom "record type", and the Model object and its fields are immutable (immutable). With TypeScript, you can simply use the interface to describe the model:

export interface IHabbitPresetsState {    presets: IHabbitPreset[];    isLoading: boolean;    isOperating: boolean;    isOperationSuccess: boolean;    isEditing: boolean;}

At this time, we need to remember in mind, never to modify the model field!

Message

The Message is used to define the events that the application may trigger during the run, for example, in a stopwatch application, we define three events such as "Start Time", "Pause Timer", "Reset". In ELM, you can use the union type to define message, and if you use TypeScript, you can define multiple message classes and then create a union type definition:

export type HabbitPresetsMsg =    Get | Receive    | Add | AddResp    | Delete | DeleteResp    | Update | UpdateResp    | BeginEdit | StopEdit;export class Get {}export class Receive {    constructor(public payload: IHabbitPreset[]) { }}export class Add {    constructor(public payload: IHabbitPreset) { }}export class AddResp {    constructor(public payload: IHabbitPreset) {    }}export class Delete {    constructor(public payload: number) {    }}export class DeleteResp {    constructor(public payload: number) { }}export class Update {    constructor(public payload: IHabbitPreset) {    }}export class UpdateResp {    constructor(public payload: IHabbitPreset) {    }}export class BeginEdit {    constructor(public payload: number) { }}export class StopEdit {}

Our application typically triggers a message from the view layer, for example, after the page has been loaded, the message "load data" is immediately triggered, and the triggered message is handled by the update module.

Update

Update, which is how the model is updated, is usually a function that describes the function with TypeScript:

update(state: IHabbitPresetsState, msg: HabbitPresetsMsg): IHabbitPresetsState

Whenever a new message is triggered, the ELM schema will take the current model of the application with the incoming message to the update function, and the execution result as the application's new model-this is the model update.
In a ELM program, the rendering of a view depends only on the data in the model, so updating the model often results in the update of the view.

View

The Elm language comes with a front-end view library, which is characterized by an update of the view that relies only on the update of the model, and almost all of the Message is triggered by the view. But in this article, I'll use ANGULAR5 to demonstrate the effect, and of course, you can use React or jQuery to implement the view, depending on your hobby.

Summary

So far, we've got a rough look at some of the main points of the ELM architecture: models, updates, views, and Message. A ELM architecture program, usually a view that triggers a specific message because of a user's action, and then computes a new model from this triggered message with the currently applied model, and the new model produces a change in the view.

Start implementing

First, let's write an empty frame:

export class ElmArch<TState, TMsgType> {}

Tstate represents the model type of the application, and Tmsgtype represents the message union type of the application.

As can be seen from the previous section, message is the key to the application being able to run, the message can be generated manually at run time, and the trigger of the message can be listened to, so you could use Rxjs/subject to build a message stream.

export class ElmArch<TState, TMsgType> {    private readonly $msg = new Subject<TMsgType>();    send(msg: TMsgType) {        this.$msg.next(msg);    }}

Here a send function is defined to better encapsulate the code, and the message flow exposes only one interface that triggers the message.

Next, we can consider the implementation of the model flow. He is very similar to the message flow, first of all to be able to be monitored, second, also receive the message can be manually generated, so you can also use Subject to achieve. But here I am using Behaviorsubject, because Behavior Subject can keep the final object, so that we can access the data in the model at any time, without the need to use Subscribe.

$res = new BehaviorSubject<TState>(initState);

At this point, 1/3 of the work has been completed, and now, according to our requirements, using RXJS let the message flow can correctly trigger the update of the model flow.

this.$msg.scan(this.update, initState)            .subscribe((s: TState) => {                    $res.next(s);            });

Scan is an operator of RXJS, similar to the Aggregate in Reduce,linq in JS. Because an initial model (Initstate) is set, the update function can receive the last computed model and the newly received message each time the message flow generates a new message, and then returns the new model. In other words, scan turns the message into a new model flow. Then subscribe to the model stream and use the previously defined Behaviorsubject to broadcast the new model.

This is close to completing 1/2 of the work, the model and message the two things have been achieved, and then continue to implement the update.

ELM is a functional language, pattern matching ability than JS do not know where to go, since to imitate the ELM architecture, then this place also to imitate out.

Type pattern<tmsg, tstate, tmsgtype> = [New (... args:any[]) = Tmsg, (acc:tstate, Msg:tmsg, $msg: SUBJECT&L T    tmsgtype>) = Tstate]; /** * Pattern matching syntax * @template tmsg * @param {new (... args:any[]) = = Tmsg} type constructor of MSG * @param {(acc:tstate, msg:tmsg, $msg:subject<tmsgtype>) = tstate} reducer method to compute new stat E * @returns {pattern<tmsg, tstate, Tmsgtype>} * @memberof Elmarch */caseof<tmsg> (type        : New (... args:any[]) = tmsg, reducer: (Acc:tstate, Msg:tmsg, $msg:subject<tmsgtype>) = tstate)    : Pattern<tmsg, Tstate, tmsgtype> {return [type, reducer]; } matchwith<tmsg> ($msg: Subject<tmsgtype>, Patterns:pattern<tmsg, Tstate, tmsgtype>[]) {Retu            RN (acc:tstate, msg:tmsg) = {Const STATE = ACC;    for (const it of patterns) {if (msg instanceof it[0]) {                return it[1] (state, MSG, $msg);        }} throw new Error (' Invalid Message Type ');    }; }

First we define a tuple type Pattern to represent the pattern matching syntax, in this case, the main need to implement is based on type matching, so the first element of the tuple is the message class, the second parameter is the callback function to execute when the match succeeds, to calculate the new model, use caseOf function to create this tuple. The matchWith return value of the function is a function that matches the scan signature of the first parameter, the first parameter is the model that was last created, and the second parameter is the received message. In this function, we find the pattern tuple that matches the received message and then use the second element of the tuple to calculate the new model.

With the above things can be better simulation mode matching function, written out like this:

const newStateAcc = matchWith(msg, [            caseOf(GetMonth, (s, m, $m) => {                // blablabla            }),            caseOf(GetMonthRecv, (s, m) => {                // blablabla            }),            caseOf(ChangeDate, (s, m) => {                // blablabla            }),            caseOf(SaveRecord, (s, m, $m) => {                // blablabla            }),            caseOf(SaveRecordRecv, (s, m) => {                // blablabla            })        ])

In this way, you need to make some changes where you used to build the model flow:

this.$msg.scan(this.matchWith(this.$msg, patterns), initState)            .subscribe((s: TState) => {                    $res.next(s);            });

Now that the model flow is built to depend on an initial state followed by a pattern array, you can encapsulate the two dependencies as arguments by using a function:

begin(initState: TState, patterns: Pattern<any, TState, TMsgType>[]) {        const $res = new BehaviorSubject<TState>(initState);        this.$msg.scan(this.matchWith(this.$msg, patterns), initState)            .subscribe((s: TState) => {                    $res.next(s);            });        return $res;    }

So far, 2/3 of the work has been done, we have designed the message flow, model flow, and processing the Update method of the message, to make a simple counter is completely no problem. Click to view the sample

But in fact, the problem we need to face is far more than one counter so simple, more cases are processing requests, and sometimes we need to process messages to trigger new messages. For asynchronous requests, you need to trigger a new message in the response of the request, you can call it directly, and you $msg.next() can call this function if you need to trigger a new message in an updated operation $msg.next() .

However, things are often not as simple as the model flow is not converted from the message flow directly through the RXJS operator, while the pattern matching part of the update function has a different execution time, which may cause the message to be inconsistent with the model update order. The solution I came up with was that for a synchronous operation to trigger a new message, it was necessary to ensure that after the current message processing was completed, the update of the model was broadcast before the new message could be triggered. Based on this guideline, I have added some code:

type UpdateResult<TState, TMsgType> = TState | [TState, TMsgType[]];/*** Generate a result of a new state with a sets of msgs, these msgs will be published after new state is published* @param {TState} newState* @param {...TMsgType[]} msgs* @returns {UpdateResult<TState, TMsgType>}* @memberof ElmArch*/nextWithCmds(newState: TState, ...msgs: TMsgType[]): UpdateResult<TState, TMsgType> {    if (arguments.length === 1) {        return newState;    } else {        return [newState, msgs];    }}

Here I have added a new type-- UpdateResult<TState, TMsgType> this type represents a tuple type of a model type or model type with a message array type. So there's a bit of a detour, and the meaning of this type of existence is that the Update function can optionally return a message to be triggered next to it, in addition to returning a new model. In this way, the simple model flow becomes the model message flow, and then, in subscribe the place where the original model flow generates a new model, the new message flow is triggered, if there is a message that needs to be triggered in the return result.

Complete code here: HTTPS://GIST.GITHUB.COM/ZEEKOZHU/C10B30815B711DB909926C172789DFD2

Using the sample

In the above Gits, a sample is mentioned, but not very complete, and then a complete example is released.

Summarize

See here, you may have found that this article implementation of the gadget looks like redux, indeed, Redux is also a JS programmer to ELM Architecture salute. By splitting the logic of a WEB application into a change of state, it helps us to better understand what we are writing, and it also allows mv* 's ideas to be further demonstrated, because when writing update-related code, you can implement business logic without touching the UI level. So, as mentioned at the beginning of this article, the view can be anything: React, Angular, JQuery, that's okay, as long as you can respond to changes in the Observable flow of the model, the DOM API is also possible, which is what is called responsive programming.

What does it mean for a common Angular application?

In my own experience of combining this gadget with Angular, the biggest change is that the code becomes more regular , especially when it comes to handling asynchronous and changing the UI, becoming easier to set up, and easier to set up, which means it's easier to generate code. Again, in Angualr, if all the inputs that the component relies on are Observable objects, you can change the default change-checking policy to: OnPush. In this way,Angular does not have to "dirty check" This component , only when the Observable update, will be to re-change components, this benefit, self-evident.

Using RxJS to implement a simple ELM architecture application

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.