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.

This folder has multiple sub folders:

main - for all of your component without lazy loading group. See routing sections for more details.

And one folder for each lazy loading group you have. Folder name matches the group name, for example: YourLazyGroupName folder for YourLazyGroupName group.

By default the source code looks like this:

export const modules = {};

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

Extending your component

Imagine you that have this component:

<?php

namespace Components\Views\CustomJs;

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

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/main/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/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/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>