All Projects → vadimv → rsp

vadimv / rsp

Licence: Apache-2.0 license
A server-state reactive Java web framework for building real-time user interfaces and UI components.

Programming Languages

java
68154 projects - #9 most used programming language
javascript
184084 projects - #8 most used programming language

Projects that are alternatives of or similar to rsp

Korolev
Single Page Applications running on the server side.
Stars: ✭ 510 (+1357.14%)
Mutual labels:  server-side-rendering, single-page-applications, fullstack
FlexDotnetCMS
A powerful, flexible, decoupled and easy to use and Fully Featured ASP .NET CMS, it can also be used as a Headless CMS
Stars: ✭ 45 (+28.57%)
Mutual labels:  server-side-rendering, single-page-applications
Reactjs Portfolio Mern Website
My Portfolio | Full Stack MERN Application
Stars: ✭ 25 (-28.57%)
Mutual labels:  server-side-rendering, single-page-applications
Nuxt Firebase Pwa
Run the Nuxt.js application (SPA * SSR * PWA) on Firebase.
Stars: ✭ 103 (+194.29%)
Mutual labels:  server-side-rendering, single-page-applications
Matestack Ui Core
Matestack enables you to create sophisticated, reactive UIs in pure Ruby, without touching JavaScript and HTML. You end up writing 50% less code while increasing productivity, maintainability and developer happiness.
Stars: ✭ 469 (+1240%)
Mutual labels:  server-side-rendering, fullstack
Stimulus reflex
Build reactive applications with the Rails tooling you already know and love.
Stars: ✭ 1,928 (+5408.57%)
Mutual labels:  server-side-rendering, single-page-applications
vitext
The Next.js like React framework for better User & Developer experience!
Stars: ✭ 376 (+974.29%)
Mutual labels:  server-side-rendering
universal
A counterpart to common package to be used with Angular Universal
Stars: ✭ 115 (+228.57%)
Mutual labels:  server-side-rendering
nextjs-ssr-isr-cdk-aws
🦄 ‏‏‎ ‎‏‏‎ ‎‏‏‎ ‎Next.js webapp using Server Side Rendering (SSR) and Incremental Static Regeneration (ISR) deployed with Serverless Nextjs CDK construct on AWS using CloudFront and Lambda@Edge
Stars: ✭ 78 (+122.86%)
Mutual labels:  server-side-rendering
Beidou
🌌 Isomorphic framework for server-rendered React apps
Stars: ✭ 2,726 (+7688.57%)
Mutual labels:  server-side-rendering
angular-httpclient
Angular 15 Example HttpClient
Stars: ✭ 21 (-40%)
Mutual labels:  server-side-rendering
universal-react-relay-starter-kit
A starter kit for React in combination with Relay including a GraphQL server, server side rendering, code splitting, i18n, SEO.
Stars: ✭ 14 (-60%)
Mutual labels:  server-side-rendering
ves
Vue SSR(Server Side Render) Web Framework for Egg
Stars: ✭ 23 (-34.29%)
Mutual labels:  server-side-rendering
rock
fullstack generator equipped with following techs: Express, Ejs, Webpack, Vue, React...
Stars: ✭ 37 (+5.71%)
Mutual labels:  fullstack
Blapy
jQuery plugin that helps you to create and manage ajax and single page web applications (SPA) with almost no javascript coding to do it
Stars: ✭ 30 (-14.29%)
Mutual labels:  single-page-applications
md-svg-vue
Material design icons by Google for Vue.js & Nuxt.js (server side support & inline svg with path)
Stars: ✭ 14 (-60%)
Mutual labels:  server-side-rendering
Live
Live views and components for golang
Stars: ✭ 251 (+617.14%)
Mutual labels:  server-side-rendering
ultimate-hot-boilerplate
🚀 node-react universal app boilerplate with everything on hot reload, SSR, GraphQL, Flow included
Stars: ✭ 35 (+0%)
Mutual labels:  server-side-rendering
Knowledge-Base
record every requirement and solution here
Stars: ✭ 31 (-11.43%)
Mutual labels:  fullstack
numvalidate
Phone number validation REST API
Stars: ✭ 54 (+54.29%)
Mutual labels:  server-side-rendering

RSP

javadoc maven version

About

RSP is a lightweight modern Java server-state web framework.

A popular approach for a Java backend based web UI is to build the client-side with tools like React or Vue and fetch the data from the server with some kind of remote API.

JavaScript client programming is extremely powerful, but this scheme introduces a lot of complexity. Any change made on the client-side potentially needs to be reflected on the API and the server-side. The project's build requires on-boarding non-Java dependency management and build tools.

RSP aims for developing the web UI in Java while keeping external dependencies and JavaScript usage to the minimum.

With RSP, after loading an initial page HTML, the browser feeds events to the server and updates the presentation to the incoming diff commands. The page's state is maintained on the server.

As the result:

  • coding and debugging the UI is just coding in plain Java and debugging Java;
  • fast initial page load no matter of the application's size;
  • your code always stays on your server;
  • SEO-friendly out of the box.

Maven

Use Java version 11 or newer.

Add the dependency:

    <dependency>
        <groupId>io.github.vadimv</groupId>
        <artifactId>rsp</artifactId>
        <version>0.7</version>
    </dependency>

Code examples

HTTP requests routing

An RSP application's initial page rendering request-response workflow consist of two explicitly defined phases:

  • routing an incoming request with a result of the page's immutable state object;
  • rendering this state object into the result HTTP response.

To dispatch the incoming request, create a Routing object and provide it as an application's constructor parameter:

    import static rsp.html.RoutingDsl.*;
    ...
    final App<State> app = new App<>(route(), render());
    ...
    private static Route<State> route()
        final var db = new Database();
        return concat(get("/articles", req -> db.getArticles().thenApply(articles -> State.ofArticles(articles))),
                      get("/articles/:id", (__, id) -> db.getArticle(id).thenApply(article -> State.ofArticle(article))),
                      get("/users/:id", (__, id) -> db.getUser(id).thenApply(user -> State.ofUser(user))),
                      post("/users/:id(^\\d+$)", (req, id) -> db.setUser(id, req.queryParam("name")).thenApply(result -> State.userWriteSuccess(result))));
    }

During a dispatch, routes verified one by one for a matching HTTP method and path pattern. Route path patterns can include zero, one or two path-variables, possibly combined with regexes and the wildcard symbol "*". The matched variables values become available as the correspondent handler functions String parameters alongside with the request details object. The route's handler function should return a CompletableFuture of the page's state:

    get("/users/*", req -> CompletableFuture.completedFuture(State.ofUsers(List.of(user1, user2))))

If needed, extract a paths-specific routing section:

    final Route<HttpRequest, State> routes = concat(get(__ -> paths()),
                                                    any(State.pageNotFound()));
    
    private static PathRoutes<State> paths() {
         return concat(path("/articles", db.getArticles().thenApply(articles -> State.ofArticles(articles))),
                       path("/articles/:id", id -> db.getArticle(id).thenApply(article -> State.ofArticle(article)));
    }

Use match() DSL function routes to implement custom matching logic, for example:

    match(req -> req.queryParam("name").isPresent(), req -> CompletableFuture.completedFuture(State.of(req.queryParam("name"))))

The any() route matches every request.

HTML markup rendering Java DSL

RSP provides a Java internal domain-specific language (DSL). This DSL is used for declarative definition of an HTML page as a composition of functions. The framework converts a DSL page definition together with a provided state object to an HTML page markup during the rendering phase.

For example, to re-write the HTML fragment below:

<!DOCTYPE html>
<html>    
    <body>
        <h1>This is a heading</h1>
        <div class="par">
            <p>This is a paragraph</p>
            <p>Some dynamic text</p>
        </div>
    </body>
</html> 

provide the Java code:

    import static rsp.html.HtmlDsl.*;
    ...
    public Component<State> render() {
        return s -> html(
                      body(
                           h1("This is a heading"),
                           div(attr("class", "par"), 
                               p("This is a paragraph"),
                               p(s.get().text)) // adds a paragraph with a text from the state object's 'text' field
                          ) 
                    );
    }

where:

  • HTML tags are represented by the rsp.html.HtmlDsl class' methods with same names, e.g. <div></div> translates to div()
  • HTML attributes are represented by the rsp.html.HtmlDsl.attr(name, value) function, e.g. class="par" translates to attr("class", "par")
  • The lambda parameter's s, s.get() method reads the current state snapshot, in this example a POJO of a State class

The utility of() DSL function renders a Stream<T> of objects, e.g. a list, or a table rows:

    import static rsp.html.HtmlDsl.*;
    ...
    s -> ul(of(s.get().items.stream().map(item -> li(item.name))))

An overloaded variant of of() accepts a CompletableFuture<S>:

    final Function<Long, CompletableFuture<String>> service = userDetailsService(); 
    ...
         // let's consider that at this moment we know the current user's Id
    s -> div(of(service.apply(s.get().user.id).map(str -> text(str))))

Another overloaded of() function takes a Supplier<S> as its argument and allows inserting code fragments with imperative logic.

    import static rsp.html.HtmlDsl.*;
    ...
    s -> of(() -> {
                     if (s.get().showInfo) {
                         return p(s.get().info);
                     } else {
                         return p("none");
                     }       
                 })

The when() DSL function conditionally renders (or not) an element:

    s -> when(s.get().showLabel, span("This is a label"))

Page state model

Model an RSP application page's state as a finite state machine (FSM). An HTTP request routing resolves an initial state. Page events, like user actions or timer events trigger state transitions.

The following example shows how a page state can be modelled using records, sealed interfaces and pattern matching in Java 17:

    sealed interface State permits UserState, UsersState {}
    record UserState(User user) implements State {}
    record UsersState(List<User> users) implements State {}
    
    record User(long id, String name) {}

    /**
     * An event handler, makes the page's FSM transition.
     */
    public static void userSelected(UseState<State> s, User user) {
        s.accept(new UserState(user));
    }

    /**
     * The page's renderer, called by the framework as a result of a state transition.
     */
    static Component<State> render() {
        return s -> switch (s.get()) {
            case UserState  state  -> userView().render(state);
            case UsersState state  -> usersView().render(state);
        };
    }

    private static Component<UserState> userView() { return s -> span("User:" + s.get()); }
    private static Component<UsersState> usersView() { return s -> span("Users list:" + s.get()); }

Single-page application

RSP supports two types of web pages:

  • Single-page application (SPA) with establishing the page's live session and keeping its state on the server
  • Plain detached pages

An RSP web application can contain a mix of both types. For example, an admin part can be a single-page application page, and the client facing part made of plain pages.

The type of page to be rendered is determined by the page's head tag DSL function.

The head() function creates an HTML page head tag for an SPA. If the head() is not present in the page's markup, the simple SPA-type header is added automatically. This type of header injects a script, which establishes a WebSocket connection between the browser's page and the server and enables reacting to the browser events.

To respond to browser events, register a page DOM event handler by adding an on(eventType, handler) to an HTML tag in the DSL:

    s -> a("#", "Click me", on("click", ctx -> {
                System.out.println("Clicked " + s.get().counter + " times");
                s.accept(new State(s.get().counter + 1));
            }));
    ...
    static class State { final int counter; State(int counter) { this.counter = counter; } }

When an event occurs:

  • the page sends the event data message to the server
  • the system fires its registered event handler's Java code.

An event handler's code usually sets a new application's state snapshot, calling one of the overloaded UseState<S>.accept() methods on the application state accessor.

A new set state snapshot triggers the following sequence of actions:

  • the page's virtual DOM re-rendered on the server
  • the difference between the current and the previous DOM trees is calculated
  • the diff commands sent to the browser
  • the page's JavaScript program updates the presentation

The event handler's EventContext class parameter has a number of utility methods.

One of these methods allows access to client-side document elements properties values by elements references.

    final ElementRef inputRef = createElementRef();
    ...
    input(inputRef,
          attr("type", "text")),
    a("#", "Click me", on("click", ctx -> {
            ctx.props(inputRef).getString("value").thenAccept(value -> System.out.println("Input's value: " + value));     
    }))

A reference to an object also can be created on-the-fly using RefDefinition.withKey() method.

There is the special window() reference for the page's window object.

The window().on(eventType, handler) method registers a window event handler:

    html(window().on("click", ctx -> {
            System.out.println("window clicked");
        }),
        ...
        )

Some types of browser events, like a mouse move, may fire a lot of invocations. Sending all these notifications over the network and processing them on the server side may cause the system's overload. To filter the events before sending use the following event object's methods:

  • throttle(int timeFrameMs)
  • debounce(int waitMs, boolean immediate)
    html(window().on("scroll", ctx -> {
            System.out.println("A throtteld page scroll event");
        }).throttle(500),
        ...
        )

The context's EventContext.eventObject() method reads the event's object as a JSON-like data structure:

    form(on("submit", ctx -> {
            // Prints the submitted form's input field value
            System.out.println(ctx.eventObject());
         }),
        input(attr("type", "text"), attr("name", "val")),
        input(attr("type", "button"), attr("value", "Submit"))
    )

Events code runs in a synchronized sections on a live page session state container object.

Plain HTML pages

Using plainHead() function instead of head() renders the markup with the head tag without injecting of this script resulting in a plain detached HTML page.

The statusCode() and addHeaders() methods enable to change result response HTTP status code and headers. For example:

    s -> html(   
              plainHead(title("404 page not found")),
              body(
                   div(
                       p("404 page not found")
                  ) 
                )
            ).statusCode(404);

UI Components

Pages are composed of components. A component is a Java class which implements the Component<S> interface.

    public static Component<ButtonState> buttonComponent(String text) {
        return s -> input(attr("type", "button"),
                           attr("class", "button"),
                           attr("value", text),      
                           on("click", ctx -> s.accept(new ButtonState())));
        
    }
    public static class ButtonState {}

The render() method of a component usually invokes render() methods of its descendant components, providing as parameters:

  • a descendant's component's state object, normally a part of the application's state object tree
  • a listener's code, a Consumer<S> implementation, which propagates the new state change from a child component to its parent, up to the root component's context
    ...
    public static Component<ConfirmPanelState> confirmPanelComponent(String text) {
        return s -> div(attr("class", "panel"),
                         span(text),
                         buttonComponent("Ok").render(new ButtonState(), 
                                                      buttonState -> s.accept(new ConfimPanelState(true))),
                         buttonComponent("Cancel").render(new ButtonState(), 
                                                          buttonState -> s.accept(new ConfimPanelState(false)));
        
    }
    public static class ConfirmPanelState {
        public final boolean confirmed;
        public ConfirmPanelState(boolean confirmed) { this.confirmed = confirmed; }
    }

An application's top-level Component<S> is the root of its component tree.

Navigation bar URL path

During a Single Page Application session, the current app state mapping to the browser's navigation bar path can be configured using the stateToPath method of an App object:

    final App<State> app = new App<>(route(),
                                     pages())  //  If the details are present, set the path to /{name}/{id} or set it to /{name}
            .stateToPath((state, prevPath) -> state.details.map(details -> Path.absolute(state.name, Long.toString(details.id)))
                                                           .or(Path.absolute(state.name)));

If not configured, the default state-to-path mapping sets an empty path for any state.

Page lifecycle events

Provide an implementation of the PageLifecycle interface as a parameter on an application's constructor. This allows to listen to the SPA page's lifecycle events:

  • before the page is created
  • after the page is closed
    final PageLifeCycle<Integer> plc = new PageLifeCycle.Default<Integer>() {
        @Override
        public void beforeLivePageCreated(QualifiedSessionId sid, UseState<Integer> useState) {
            final Thread t = new Thread(() -> {
                try {
                    Thread.sleep(10_000);
                    synchronized (useState) {
                        useState.accept(useState.get() + 1);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            t.start();
        }
    };
    
    final App<Optional<FullName>> app = new App<>(route(), pages()).pageLifeCycle(plc);
    ...

Add these listeners, for example, when you need to subscribe to some messages stream on a page live session creation and unsubscribing when the page closes.

Application and server's configuration

Provide an instance of the rsp.AppConfig class as the parameter to the config method of an App object:

    final var app = new App(routes(), rootComponent()).config(AppConfig.DEFAULT);

A web server's rsp.jetty.JettyServer class constructor accepts parameters like the application's web context base path, an optional static resources' handler and a TLS/SSL connection's configuration:

    final var staticResources = new StaticResources(new File("src/main/java/rsp/tetris"), "/res/*");
    final var sslConfig = SslConfiguration("/keysore/path", "changeit");
    final var server = new JettyServer(8080, "/base", app, staticResources, sslConfig);
    server.start();
    server.join();

Logging

This project's uses System.Logger for server-side logging.

On the client-side, to enable detailed diagnostic data exchange logging, enter in the browser console:

  RSP.setProtocolDebugEnabled(true)

Schedules

The EventContext.schedule() and EventContext.scheduleAtFixedRate() methods allow submitting of a delayed or periodic action that can be cancelled. A timer reference parameter may be provided when creating a new schedule. Later this reference could be used for the schedule cancellation. Scheduled tasks will be executed in threads from the internal thread pool, see the synchronized versions of accept() and acceptOptional() methods of the live page object accepting lambdas as parameters.

    final static TimerRef TIMER_0 = TimerRef.createTimerRef();
    ...
    button(attr("type", "button"),
           text("Start"),
           on("click", c -> c.scheduleAtFixedRate(() -> System.out.println("Timer event")), TIMER_0, 0, 1, TimeUnit.SECONDS))),
    button(attr("type", "button"),
               text("Stop"),
               on("click", c -> c.cancelSchedule(TIMER_0)))

How to build the project and run tests

To build the project from the sources:

$ mvn clean package

To run all the tests:

$ mvn clean test -Ptest-all
Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].