Extending with JavaScript

Having you component in PHP is awesome, but that does not limit you to it.

It makes perfect sense that wou would like to use some extra JavaScript with so many libraries available out there. Like UI tools for drag-and-drop, animations, charts, etc.

That stuff is not important for server-side rendering since SEO does not care, but you user will certainly appreciate some extra features.

Autogenerated JavaScript

As it was already mentioned in a introduction section, your PHP components are being converted into their JavaScript counterparts. These files are located at /viewi-app/js/app folder. This folder is getting overridden each time you build your Viewi project.

But there is also another folder that is created specifically for your custom code: /viewi-app/js/modules/your_name.

your_name is your application name from configuration $viewiConfig = (new AppConfig('your_name')).

Keep/move your custom JavaScript implementation relative to this folder.

The name of your application should be ([a-zA-Z-_]*). 'default' is default app name.

By default the source code looks like this:

index.ts

export const modules = {};

It is a typescript but it is optional and you can use a simple JavaScript.

Standardizing purpose and Viewi package support.

For example, code from some package /vendor/package/src/viewi-app/js/modules/your_app_name will get exported to viewi-app/js/exports/your_app_name if you use that package.

Extending your component

Mark your component with ExtendWithJs attribute.

Imagine you that have this component:

<?php

namespace Components\Views\CustomJs;

use Viewi\Components\Attributes\LazyLoad;
use Viewi\Components\BaseComponent;
use Viewi\Builder\Attributes\ExtendWithJs;

#[ExtendWithJs]
class CustomJsPage extends BaseComponent
{
    public string $title = 'Custom JS page with lazy loading';
    public string $markText = "some text \n\n# Marked in browser\n\nRendered by **marked**.";

    public function getMarkedHtml($text)
    {
        return "";
    }
}

And you want your front-end to render markdown content with marked js library.

You got to your JavaScript source code:

cd ./viewi-app/js

Then install NPM package:

npm install marked

And now you want to use that.

Open your /viewi-app/js/modules/your_name/index.ts file and modify it. You can create folders and other files in the modules folder, it is up to you.

To extend CustomJsPage page we need to import it from our app folder:

import { CustomJsPage } from '../../app/main/components/Components/Views/CustomJs/CustomJsPage';

Then we need our marked library:

import { marked } from 'marked';

Now, the extending itself. You can change a JavaScript class by adding or modifying methods or properties within a prototype of the class. For example, to modify getMarkedHtml method you need to reassign it in prototype:

CustomJsPage.prototype.getMarkedHtml = function (this: CustomJsPage) {
    // new logic
};

And last, you need to export these changes by adding CustomJsPage to modules export:

// Add CustomJsPage to this
export const modules = { /** here **/ };
export const modules = { CustomJsPage };

Final result:

import { marked } from 'marked';
import { CustomJsPage } from '../../app/main/components/Views/CustomJs/CustomJsPage';

CustomJsPage.prototype.getMarkedHtml = function (this: CustomJsPage) {
    return marked(this.markText);
};

export const modules = { CustomJsPage };

Now run the build and you can see marked library working in the browser.

Server-side rendering consideration

Couple of the important things to remember while working with custom JavScript.

Always wrap custom JavaScript output in the template to avoid hydration failure:

Bad

{{getMarkedHtml($markText)}} will break the page on client-side since there will be a mismatch during hydration process while trying to match server-side content.

Server renders an empty string (or could be worse, renders something different). While client-side is trying to match newly generated markdown with the server variant.

<Layout title="$title">
    <h1>$title</h1>
    <h3>Input</h3>
    <label for="markdown-text">
        Enter some markdown
    </label>
    <div>
        <textarea id="markdown-text" rows="10" model="$markText" style="width: 100%;"></textarea>
    </div>
    <h3>Input:</h3>
    <div>
        <pre>$markText</pre>
    </div>
    <h3>Output:</h3>
    {{getMarkedHtml($markText)}}
</Layout>

Good

Always wrap you custom output into another tag.

<div>{{getMarkedHtml($markText)}}</div>
<Layout title="$title">
    <h1>$title</h1>
    <h3>Input</h3>
    <label for="markdown-text">
        Enter some markdown
    </label>
    <div>
        <textarea id="markdown-text" rows="10" model="$markText" style="width: 100%;"></textarea>
    </div>
    <h3>Input:</h3>
    <div>
        <pre>$markText</pre>
    </div>
    <h3>Output:</h3>
    <div>{{getMarkedHtml($markText)}}</div>
</Layout>

Complete JavaScript rewrite

If you want to implement JavaScript version completely from scratch, mark your component with CustomJs attribute.

Export your JavaScript implementation in your index.ts file.