The Editor
Welcome back, thanks for sticking around, This part focuses on the Editor, honestly its more vue than the editor, we will setup few components and implement logical routing, local storage and post request to the simple server,
Note: because there is lot of code for this part, I will post all the code according their files in the bottom of the article, so when you get stuck etc
Header Component
under components create Header.vue, this component will be responsible for our routing, basically determines what is shown in the main window of app.vue
create the three tags
<template>
</template>
<script>
export default{
name: "Header"
}
</script>
Import Header.vue into App.vue and remove Ed
App.vue
<template>
<Header/>
</template>
<script>
import Header from "./components/Header.vue";
export default{
name: "App",
components: {
Header
}
}
</script>
<style scoped>
.container{
padding: .4em 1em;
}
</style>
This is how the new App.vue is setup, the editor should disappear, and now we have an empty window.
Because now the Header is a child of App.vue and also responsible for routing, we need a way to communicate with the parent component, this is where emit comes in.
App.vue
Header.vue
this is how the order looks like, header as a child of app.vue, emit allows the child component to send a signal to it's parent, and a parent can catch that signal, with the signal we can send props too, as we will see.
In the header we have Home, Editor and Job Board, we want on click on any of these three header to send a signal to App.vue about which button was clicked, so App.vue will display the relevant component.
However we do not want to display the Editor component directly from Vue.app, because we need to manage templates, like add new template, delete template, editor will be a child of manage component, ultimately the structure will look like this
App.vue
Header.vue
Home.vue
EditorManage.vue
Editor.vue
OK create the other two components Home and Manage(EditorManager) in the components folder, we already have Editor.vue which we will navigate to from Manage component.
Back to the header
<template>
<div class="header">
<div>
<h1>Logo</h1>
</div>
<nav>
<ul>
<!-- emitting a signal called Header, with a prop -->
<li @click="$emit('Header', 'Home')" >Home</li>
<li @click="$emit('Header', 'Editor')">Editor</li>
<li>Job Board</li>
</ul>
</nav>
</div>
</template>
<script>
export default {
name: "Header"
}
</script>
For now we will setup home which is an empty component and Manage our focus for this article
as you can see emit is just a function, the first variable "Header" is the name of the signal, the parent component uses this name to catch the signal whenever it is fire,
after the signal name follows normal parameters, in this case we pass a string, which we will use to determine which component to show
"$emit('Header', 'Home')"
we use basic colors to style the components, just so we can see what we are doing, as I mentioned in the previous post I will not explain css as much,
css for header
<template>
....
</template>
<script>
export default {
name: "Header"
}
</script>
//css
<style scoped>
.header {
display:flex;
justify-content: space-between;
color: white;
background-color: black;
margin-bottom: 2em;
}
.header h1 {
padding-left: 1em;
}
.header nav ul {
display: flex;
margin: 1em 3em;
}
.header nav ul li {
padding: 1em 2em;
list-style: none;
}
li:hover{
cursor: pointer;
}
</style>
this will apply basic styling to the header just for construct
Catching and Handling Signals
In the App.vue let's handle header signals, it is very simple, you pass or define the name of the signal in the firing component like a prop and assign a function to handle the signal, or to execute when the signal is fired
@name-of-the-signal = "handlingFunction"
In App.vue
<template>
<!-- handling the signal fired by heard, which executes navigate() -->
<Header @Header="navigate"/>
</template>
Vue has a built in method called data() which returns an object, with data, this data think of it as the state of the component, accessible anywhere in the component it's self, we will see more examples later, for now we define a var called displayed which will hold the val or second param from emit(the strings we passed), as you can see below, we pass a param route to the navigate(route) function, route will equal the value passed in the emit function, either "Home" || "Manage" and based on the string passed will show the relevant component
</template>
<script>
import Header from "./components/Header.vue";
export default {
name: "App",
components: {
Header
},
methods: {
navigate(route){
this.display = route
}
},
data(){
return{
// has a default value of Home, so on enter the app always show the home component
display: "Home"
}
}
};
</script>
import the Home and Manage components to App.vue
<script>
import Header from "./components/Header.vue";
import Home from "./components/Home.vue";
import Manage from "./components/EditorManager.vue";
export default {
name: "App",
components: {
Header,
Home,
Manage
},
...
};
</script>
Let's setup conditional routing, in the template, whenever the display var updates. meaning the user is navigating, the component will update, cause on nav the emitted signal updates state
in App.vue:
<template>
<div class="container">
<!-- header is always rendered -->
<Header @Header="navigate"/>
<!-- Manage will render if the display var === string Editor -->
<div v-if="display === 'Editor'">
<Manage/>
</div>
<!-- so does home, this is called conditional rendering, v-if same as if -->
<div v-if="display === 'Home'">
<Home/>
</div>
</div>
</template>
We that App.vue is complete,
you should be able to "route" now, or explicitly logical render
Manage component
In EditorManager.vue
setup the defaults
<template>
<div>
</div>
</template>
<script>
export default{
name: "Manage"
}
</script>
<style scoped>
</style>
Firstly we need a way to add new templates, we will use a floating action button(fab), place at the bottom right, onClick it will open a modal to fill the title and subtitle of the temp, save, also it the modal is opened the same button will close it,
First create a Modal component, call the File AddTemp, for adding template, and the component itself add, and import it to Editor Manger, and declare inside a div with class modal, as shown below, also we need a data() function, to update the showModal boolean value, which initial will be false
<template>
<div class="modal" v-if="showModal">
<!-- addTemplate is a signal coming from the Add component -->
<Add @addTemplate="add"/>
</div>
</template>
<script>
import Add from "./AddTemp.vue"
//import the editor also
import Ed from "./Editor.vue"
export default{
name: "Manage",
components: {
Add,
Ed
}
}
</script>
data()
...
export default{
name: "Manage",
components:{
...
},
data(){
return{
showModal: false,
}
},
}
let's create a fab button to toggle the modal
<template>
<div>
<div class="fab" @click="toggleModal">
// show add when modal is closed
<label v-if="!showModal">
add
</label>
//show close when modal is open
<label v-if="showModal">
close
</label>
</div>
</div>
<div class="modal" v-if="showModal">
....
</div>
</template>
<script>
....
</script>
<style scoped>
/*place the fab bottom right and make it black */
.fab{
position: absolute;
padding: 1em;
background: black;
color: white;
bottom: 0;
right: 0;
margin: 1em;
border-radius: 30px;
/* right: 100%; */
}
/* expand the fab on hover */
.fab:hover {
transition : transform 200ms ease-out;
transform: scale(1.1, 1.2)
}
/* styles the modal, center it, give a box-shadow */
.modal {
position: absolute;
top: 25%;
width: 50%;
transform: translateX(50%);
display: flex;
justify-content: center;
box-shadow: 15px 15px 53px rgb(243, 244, 246);
border-radius: .5em;
max-height: 30em;
background: lightblue;
}
</style>
let's add the logic to toggleModal
<script>
export default{
...,
methods: {
toggleModal(){
//make showModal the opposite of itself
//this will toggle the modal
this.showModal = !this.showModal
},
}
}
</script>
Now open the AddTemp file and fill it with the default tags,
for the template its a simple form, accepting title and subtitle
<template>
<div>
<h4>Add Template</h4>
<div class="form">
<label>Title</label>
<input v-model="title"/>
<hr>
<label>SubTitle</label>
<input v-model="SubTitle"/>
// submit the form by emitting a signal (the signal will be emitted in the addTemplate() function)
<button @click="addTemplate()">Add</button>
</div>
</div>
</template>
v-model creates what we call a controlled element - simply an element bound to a certain state, when it changes the components is affected., in the inputs we bind title and SubTitle which we will define in the data() function.
<script>
export default{
name: "Add",
data(){
//state(bound to the input elements)
return {
title: "",
SubTitle: ""
}
}
</script>
preparing data and signaling so manage can save the created template
<script>
export default{
name: "Add",
data(){
...
},
methods: {
addTemplate(){
// get the form data
const temp = {
title: this.title,
SubTitle: this.SubTitle
}
//signal and give EditorManager the temp data
this.$emit("addTemplate", temp)
},
}
</script>
<style scoped>
.form{
width: 100%;
display: grid;
gap: .5em;
}
.form input{
border: 2px solid black;
width: 100%;
height: 22px;
}
.form button{
margin: 1em;
}
</style>
handling signal and temp data in Editor Manager
<script>
...
//we will define these methods shortly
import {persist} from "../utillity/localPersist"
import {getTemps} from "../utillity/localPersist"
export default{
name: "Manage",
...,
methods: {
add(template){
console.log(template)
// creating a numeric id
template.id = this.templates.length + 1;
// adding the new template to the existing ones
this.templates = [...this.templates, template]
// we will define shortly persist the data to localstorage(browser store)
persist(this.templates)
},
}
}
</script>
in utilities create localPersist file and add the following functionality
// persist data
export function persist(templates){
try {
// persist templates with key templates, and we are stringifying the templates because localstorage can only store strings
localStorage.setItem("templates", JSON.stringify(templates))
} catch (error) {
console.log(error)
}
}
// get template data
export function getTemps(){
// get string data and parsing back to object
return JSON.parse(localStorage.getItem("templates"))
}
now you should be able to persist template data, we can use the created method to fetch the saved templates,
in EditorManager:
export default{
name: "Manage",
data(){
...,
templates: []
},
methods: {
....
//way to delete templates
del(id) {
// del a template given an id
this.templates.splice(id-1, 1)
// save the new data
persist(this.templates)
},
}
created(){
// if not undefined || null
if(getTemps()){
// asigning templates to templates[]
this.templates = getTemps();
}
}
Let's visualize the templates and setup, the necessary buttons
in EditorManager
<template>
<div>
<div class="templates">
<!--looping over templates -->
<!--:key - unique -->
<div v-for="template in templates" :key="template.title" class="template">
<div class="temp__text">
<h2>{{template.title}}</h2>
<h3>{{template.SubTitle}}</h3>
</div>
// each template controls
<div class="actions">
// will not implement in this article
<button @click="edit">Edit</button>
<button @click="del(template.id)">Del</button>
// open Ed to create the specified template
<button @click="openEditor(template)">Editor</button>
</div>
</div>
</div>
<div class="fab">
....
</div>
</div>
<div class="modal" ..>
....
</div>
</template>
css for templates
.template {
display: grid;
grid-template-columns: 50% 50%;
color: lightblue;
}
.temp__text {
display: flex;
padding: .5em;
justify-content: space-around;
}
.actions{
display:flex;
align-items: center;
}
.actions button {
padding: .5em 1em;
/* height: 1.5em; */
margin-left: 1em;
background: black;
color: white;
border-radius: 15px;
}
.actions button:hover {
transition: tranform 200ms ease-out;
transform: scale(1.1, 1.2);
}
Opening the Editor
setup
<template>
//v-if="!showEditor" === show the templates only when editor is closed
<div v-if="!showEditor">
<div class="templates">
....
</div>
<div class="fab">
...
</div>
</div>
<div class="modal" v-if="showModal">
...
</div>
<div v-if="showEditor">
// showing editor, passing a boud props :data(which is the selected template)
//@back signals back(when clicking a back button on Ed to close the Editor)
<Ed @back="closeEd()" :data="temp"/>
</div>
</template>
<script>
export default{
...,
data(){
return {
...,
showEditor: false,
temp: undefined,
}
},
methods: {
...,
openEditor(template){
// if the modal is opened close it
if(this.showModal){
this.toggleModal()
}
// the :data prop passed to Ed
this.temp = template;
// show the editor
this.showEditor = true;
},
// on signal back button on Ed close the editor
closeEd(){
window.editor = undefined;
this.showEditor = false;
},
}
}
This is all for the new components,
for the home component, you can make whatever you like, for me it'll be simple tutorial on how to use the webapp
In Editor.vue a little changed
first add : the back button under the div with class editorTools and update props to take a prop data of type Object(which is the selected template)
<template>
<div class="editorTools">
..
</div>
<!--emit signal back which closes editor, back to manager -->
<button @click="$emit('back')" >Back</button>
<!-- make the data prop text of h1 element(for now)-->
<h1>{{ data }}</h1>
</template>
<script>
export default{
...,
props:{
data: Object
},
methods: {
save: function(){
window.editor.save().then((data)=> {
// publishing the data to the server
let newB = {
id: this.data.id,
title: this.data.title,
subtitle: this.data.SubTitle,
data
}
// stringify
let strData = JSON.stringify(newB)
console.log(strData)
console.log(newB)
// posting to the local our simple save the published data
fetch(`http://localhost:3000/temp/new/${strData}`, {method: "POST"}).then(res => {
console.log(res.text().then(data => console.log(data)))
})
})
}
}
}
</script>
And that's it for now, but one last thing if you noticed, publishing an editor with an image fails, the browser does not allow it, because the base64 string is long, this we will solve in the next article before the ionic stuff, I still need to research about it a bit more, and find an efficient way,
for now you can write a template which is text and publish it, the local server will respond.
I made few changes to the local server, you can copy it below.
const express = require("express")
const jsonServer = require("json-server")
const app = express()
let Templates = {}
const router = jsonServer.router("db.json")
const middlewares = jsonServer.defaults()
const server = jsonServer.create()
server.use(middlewares)
server.get('/home', (req, res) => {
res.jsonp({ user: 'tj' });
})
server.get("/temp/:id", (req, res)=> {
let {id} = req.params
let getted = Templates[id]
console.log(getted)
res.jsonp({data: getted})
})
server.post("/temp/new/:data", (req, res)=> {
let {data} = req.params
data = JSON.parse(data)
Templates[data.id] = data
console.log(Templates)
console.log(data.data.blocks[0].data.img)
res.status(200).jsonp(Templates);
} )
// router.render = (req, res) => {
// res.jsonp({
// body: res.locals.data
// })
// }
server.use(router)
server.use(jsonServer.bodyParser)
server.use((req, res, next) => {
if (req.method === 'POST') {
req.body.createdAt = Date.now()
}
// Continue to JSON Server router
next()
})
server.listen(3000, ()=> {
console.log(`listening on port ${3000}`)
})
All the code(for affected files)
App.vue:
<template>
<div class="container">
<Header @Header="navigate"/>
<div v-if="display === 'Editor'">
<!-- <Ed msg="Editor" /> -->
<Manage/>
</div>
<div v-if="display === 'Home'">
<Home/>
</div>
</div>
</template>
<script>
// import Ed from "./components/Editor.vue";
import Header from "./components/Header.vue";
import Home from "./components/Home.vue";
import Manage from "./components/EditorManager.vue";
export default {
name: "App",
components: {
Header,
Home,
Manage
},
methods: {
navigate(route){
this.display = route
}
},
data(){
return{
display: "Home"
}
}
};
</script>
<style scoped>
.container{
padding: .4em 1em;
}
</style>
Header.vue
<template>
<div class="header">
<div>
<h1>Logo</h1>
</div>
<nav>
<ul>
<li @click="$emit('Header', 'Home')" >Home</li>
<li @click="$emit('Header', 'Editor')">Editor</li>
<li>Job Board</li>
</ul>
</nav>
</div>
</template>
<script>
export default {
name: "Header"
}
</script>
<style scoped>
.header {
display:flex;
justify-content: space-between;
color: white;
background-color: black;
margin-bottom: 2em;
}
.header h1 {
padding-left: 1em;
}
.header nav ul {
display: flex;
margin: 1em 3em;
}
.header nav ul li {
padding: 1em 2em;
list-style: none;
}
li:hover{
cursor: pointer;
}
</style>
EditorManager.vue
<template>
<div v-if="!showEditor">
<div class="templates">
<div v-for="template in templates" :key="template.title" class="template">
<div class="temp__text">
<h2>{{template.title}}</h2>
<h3>{{template.SubTitle}}</h3>
</div>
<div class="actions">
<button @click="edit">Edit</button>
<button @click="del(template.id)">Del</button>
<button @click="openEditor(template)">Editor</button>
</div>
</div>
</div>
<div class="fab" @click="toggleModal">
<label v-if="!showModal">
add
</label>
<label v-if="showModal">
close
</label>
</div>
</div>
<div class="modal" v-if="showModal">
<Add @addTemplate="add"/>
</div>
<div v-if="showEditor">
<Ed @back="closeEd()" :data="temp"/>
</div>
</template>
<script>
import Add from "./AddTemp.vue"
import Ed from "./Editor.vue"
import {persist} from "../utillity/localPersist"
import {getTemps} from "../utillity/localPersist"
export default {
name: "Manage",
components: {
Add,
Ed
},
data(){
return{
showModal: false,
showEditor: false,
temp: undefined,
templates: []
}
},
methods:{
toggleModal(){
this.showModal = !this.showModal
},
closeEd(){
window.editor = undefined;
this.showEditor = false;
},
add(template){
console.log(template)
template.id = this.templates.length + 1;
this.templates = [...this.templates, template]
persist(this.templates)
this.templates.forEach(val => {
console.log(val.title)
})
},
del(id) {
this.templates.splice(id-1, 1)
persist(this.templates)
},
edit(){
},
openEditor(template){
if(this.showModal){
this.toggleModal()
}
this.temp = template;
this.showEditor = true;
}
},
created(){
console.log(getTemps())
if(getTemps()){
console.log("not und")
this.templates = getTemps();
}
}
}
</script>
<style scoped>
.fab{
position: absolute;
padding: 1em;
background: black;
color: white;
bottom: 0;
right: 0;
margin: 1em;
border-radius: 30px;
/* right: 100%; */
}
.fab:hover {
transition : transform 200ms ease-out;
transform: scale(1.1, 1.2)
}
.modal {
position: absolute;
top: 25%;
width: 50%;
transform: translateX(50%);
display: flex;
justify-content: center;
box-shadow: 15px 15px 53px rgb(243, 244, 246);
border-radius: .5em;
max-height: 30em;
background: lightblue;
}
.template {
display: grid;
grid-template-columns: 50% 50%;
color: lightblue;
}
.temp__text {
display: flex;
padding: .5em;
justify-content: space-around;
}
.actions{
display:flex;
align-items: center;
}
.actions button {
padding: .5em 1em;
/* height: 1.5em; */
margin-left: 1em;
background: black;
color: white;
border-radius: 15px;
}
.actions button:hover {
transition: tranform 200ms ease-out;
transform: scale(1.1, 1.2);
}
.templates{
}
</style>
AddTemp.vue
<template>
<div>
<h4>Add Template</h4>
<div class="form">
<label >Title</label>
<input v-model="title"/>
<hr>
<label>SubTitle</label>
<input v-model="SubTitle"/>
<button @click="addTemplate()">Add</button>
</div>
</div>
</template>
<script>
export default {
name: "Add",
props: {
},
data(){
return {
title: "",
SubTitle: ""
}
},
methods: {
addTemplate(){
const temp = {
title: this.title,
SubTitle: this.SubTitle
}
this.$emit("addTemplate", temp)
},
}
}
</script>
<style scoped>
.form{
width: 100%;
display: grid;
gap: .5em;
}
.form input{
border: 2px solid black;
width: 100%;
height: 22px;
}
.form button{
margin: 1em;
}
</style>
That's all for now, thanks for reading,
next we will fix the server issue, although i encourage you to fix it on your own, and then we will implement the ionic part
questions or want to to say hi, the best way is twitter: