Integrate a heavy Vue Component

We can abstract any complicated of server side render component with htmlgo. But a lots of components in the modern web have done many things on the client side. means there are many logic happens before the it interact with server side.

Here is an example, a rich text editor. you have a toolbar of buttons that you can interact, most of them won't need to communicate with server. We are going to integrate the fantastic rich text editor tiptap to be used as any htmlgo.HTMLComponent.

Step 1: Create a @vue/cli project:

$ vue create tiptapjs

Modify or add a separate vue.config.js config file,

const {defineConfig} = require('@vue/cli-service');
module.exports = defineConfig({
	transpileDependencies: true,
	runtimeCompiler: true,
	productionSourceMap: false,
	devServer: {
		port: 3500,
	},
	configureWebpack: {
		output: {
			libraryExport: 'default',
		},
		externals: {vue: 'Vue'},
	},
	chainWebpack: config => {
		const svgRule = config.module.rule('svg').clear();
		svgRule.
				test(/\.(svg)(\?.*)?$/).
				use('babel-loader').
				loader('babel-loader').
				end().
				use('vue-svg-loader').
				loader('vue-svg-loader');
	},
});
  • Enable runtimeCompiler so that vue can parse template html generate from server.
  • Made Vue as externals so that it won't be packed to the dist production js file, Since we will be sharing one Vue.js for in one page with other libraries.
  • Config svg module to inline the svg icons used by tiptap

Step 2: Create a vue component that use tiptap

Install tiptap and tiptap-extensions first

$ yarn add tiptap tiptap-extensions

And write the editor.vue something like this, We omitted the template at here.

export default {

  components: {
    EditorContent,
    EditorMenuBar,
    Icon,
  },

  props: {
    value: String,
  },

  data() {
    return {
      editor: new Editor({
        content: this.$props.value,
        extensions: extensions(),
        onUpdate: ({getHTML}) => {
          const html = getHTML();
          this.$emit("input", html)
        },
      })
    }
  },

  beforeDestroy() {
    this.editor.destroy()
  }
}

We injected the this.$plaid(). that is from web/corejs, Which you will need to use For every Go Plaid web applications. Here we uses one function fieldValue from it. It set the form value when the rich text editor changes. So that later when you call EventFunc it the value will be posted to the server side. Here we will post the html value. Also allow component user to set fieldName, which is important when posting the value to the server.

Step 3: At main.js, Use a special hook to register the component to web/corejs

import TipTapEditor from './editor.vue'

(window.__goplaidVueComponentRegisters =
	window.__goplaidVueComponentRegisters || []).push((Vue) => {
		Vue.component('tiptap-editor', TipTapEditor)
	});

Step 4: Test the component in a simple html

We edited the index.html inside public to be the following:

<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width,initial-scale=1.0">
	<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
	<title>tiptapjs</title>
</head>

<body>

	<div id="app">
		<tiptap-editor field-name="content1" value='&lt;h1&gt;header&lt;/h1&gt;&lt;p&gt;abc&lt;/p&gt;'></tiptap-editor>

	</div>
	<script src="http://localhost:3500/app.js"></script>
	<script src="http://localhost:3100/app.js"></script>

</body>
  • For http://localhost:3500/app.js to be able to serve. you have to run yarn serve in tiptapjs directory.
  • http://localhost:3100/app.js is goplaid web corejs vue project. So go to that directory and run yarn serve to start it. and then in
  • Run a web server inside tiptapjs directory like python -m SimpleHTTPServer and point your Browser to the index.html file, and see if your vue component can render and behave correctly.

Step 5: Use packr to pack the dist folder

We write a packr box inside tiptapjs.go along side the tiptapjs folder.

import (
	"embed"
	"github.com/goplaid/web"
)

//go:embed tiptapjs/dist
var box embed.FS

func JSComponentsPack() web.ComponentsPack {
	v, err := box.ReadFile("tiptapjs/dist/tiptap.umd.min.js")
	if err != nil {
		panic(err)
	}

	return web.ComponentsPack(v)
}

func CSSComponentsPack() web.ComponentsPack {
	v, err := box.ReadFile("tiptapjs/dist/tiptap.css")
	if err != nil {
		panic(err)
	}

	return web.ComponentsPack(v)
}

And write a build.sh to build the javascript to production version, and run packr to pack them into a_tiptap-packr.go file.

CUR=$(pwd)/$(dirname $0)

if test "$1" = 'clean'; then
    echo "Removing node_modules"
    rm -rf $CUR/tiptapjs/node_modules/
fi

rm -r $CUR/tiptapjs/dist
echo "Building tiptapjs"
cd $CUR/tiptapjs && npm install && npm run build

Step 6: Write a Go wrapper to wrap it to be a HTMLComponent

import (
	"context"

	"github.com/goplaid/web"
	h "github.com/theplant/htmlgo"
)

type TipTapEditorBuilder struct {
	tag *h.HTMLTagBuilder
}

func TipTapEditor() (r *TipTapEditorBuilder) {
	r = &TipTapEditorBuilder{
		tag: h.Tag("tiptap-editor"),
	}

	return
}

func (b *TipTapEditorBuilder) FieldName(v string) (r *TipTapEditorBuilder) {
	b.tag.Attr(web.VFieldName(v)...)
	return b
}

func (b *TipTapEditorBuilder) Value(v string) (r *TipTapEditorBuilder) {
	b.tag.Attr(":value", h.JSONString(v))
	return b
}

func (b *TipTapEditorBuilder) MarshalHTML(ctx context.Context) (r []byte, err error) {
	return b.tag.MarshalHTML(ctx)
}

Step 7: Use it in your web app

To use it, first we have to mount the assets into our app

mux.Handle("/assets/tiptap.js",
	web.PacksHandler("text/javascript",
		tiptap.JSComponentsPack(),
	),
)

mux.Handle("/assets/tiptap.css",
	web.PacksHandler("text/css",
		tiptap.CSSComponentsPack(),
	),
)

And reference them in our layout function.

func tiptapLayout(in web.PageFunc) (out web.PageFunc) {
	return func(ctx *web.EventContext) (pr web.PageResponse, err error) {
		addGA(ctx)

		ctx.Injector.HeadHTML(`
			<link rel="stylesheet" href="/assets/tiptap.css">
			<script src='/assets/vue.js'></script>
		`)

		ctx.Injector.TailHTML(`
<script src='/assets/tiptap.js'></script>
<script src='/assets/main.js'></script>
`)
		ctx.Injector.HeadHTML(`
		<style>
			[v-cloak] {
				display: none;
			}
		</style>
		`)

		var innerPr web.PageResponse
		innerPr, err = in(ctx)
		if err != nil {
			panic(err)
		}

		pr.Body = innerPr.Body

		return
	}
}

And we write a page func to use it like any other component:

import (
	"github.com/goplaid/web"
	"github.com/goplaid/x/tiptap"
	. "github.com/theplant/htmlgo"
	"github.com/yosssi/gohtml"
)

func HelloWorldTipTap(ctx *web.EventContext) (pr web.PageResponse, err error) {

	defaultValue := ctx.R.FormValue("Content1")
	if len(defaultValue) == 0 {
		defaultValue = `
			<h1>Hello</h1>
			<p>
				This is a nice editor
			</p>
			<ul>
			  <li>
				<p>
				  123
				</p>
			  </li>
			  <li>
				<p>
				  456
				</p>
			  </li>
			  <li>
				<p>
				  789
				</p>
			  </li>
			</ul>
`
	}

	pr.Body = Div(
		tiptap.TipTapEditor().
			FieldName("Content1").
			Value(defaultValue),
		Hr(),
		Pre(
			gohtml.Format(ctx.R.FormValue("Content1")),
		).Style("background-color: #f8f8f8; padding: 20px;"),
		Button("Submit").Style("font-size: 24px").
			Attr("@click", web.POST().EventFunc("refresh").Go()),
	)

	return
}

func refresh(ctx *web.EventContext) (er web.EventResponse, err error) {
	er.Reload = true
	return
}

var HelloWorldTipTapPB = web.Page(HelloWorldTipTap).
	EventFunc("refresh", refresh)

const HelloWorldTipTapPath = "/samples/hello_world_tiptap"

And now let's check out our fruits: