DEV Community

sk
sk

Posted on • Updated on

From the Ionic framework to market(well sort of) part 3

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>




Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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 




Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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')"
Enter fullscreen mode Exit fullscreen mode

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>



Enter fullscreen mode Exit fullscreen mode

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"

Enter fullscreen mode Exit fullscreen mode

In App.vue

<template>
      <!--  handling the signal fired by heard, which executes navigate() -->
     <Header @Header="navigate"/>

</template>




Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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>



Enter fullscreen mode Exit fullscreen mode

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>




Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

data()



...
export default{
name: "Manage",
components:{
...
},

 data(){

     return{

         showModal: false,
     }

 },



}


Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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>



Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>



Enter fullscreen mode Exit fullscreen mode

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"))

}


Enter fullscreen mode Exit fullscreen mode

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();



 }

}

Enter fullscreen mode Exit fullscreen mode

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>



Enter fullscreen mode Exit fullscreen mode

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);

}




Enter fullscreen mode Exit fullscreen mode

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;

 },

}



}



Enter fullscreen mode Exit fullscreen mode

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>



Enter fullscreen mode Exit fullscreen mode

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}`)
})

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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>



Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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:

Twitter URL

Latest comments (0)