Learn To Create Your Own Salesforce App

Salesforce Tutorial: Learn To Create Your Own Salesforce App

In the previous blogs, you learnt what is Salesforce and different Salesforce certification. In this Salesforce tutorial blog, I will show you how to create a custom Salesforce App. I will be creating an app called StudentForce which can be used to maintain student records.

This app will contain three different objects (tables) to store data. The first object called Students Data will contain the names of students and their personal details like email id, phone number and native city. The college, students belong to, will be stored in the second object called College and the third object called Marks will contain the marks obtained by the students in various subjects.

Salesforce Tutorial

I have covered the following topics in this Salesforce tutorial blog with step-by-step instructions and screenshots:

  • How to create the app environment?
  • What are tabs and how to create tabs in your app?
  • What are profiles and how to customize user profiles?
  • How to create objects in the app?
  • How to create fields in objects and define their data type?
  • How to add entries (fields) into these objects?
  • How to link (create a relationship between) two different objects?

Before I get started with creating an app, let me introduce you to the cloud environment where Salesforce apps are built.

Salesforce Org

The cloud computing space offered to you or your organization by Force.com is called Salesforce org. It is also called Salesforce environment. Developers can create custom Salesforce Apps, objects, workflows, data sharing rules, Visualforce pages and Apex coding on top of Salesforce Org. Get your Salesforce sales cloud certification today to become certified.

Let us now deep dive into Salesforce Apps and understand how it functions.

Salesforce Apps

The primary function of a Salesforce app is to manage customer data. Salesforce apps provide a simple UI to access customer records stored in objects (tables). Apps also help in establishing relationship between objects by linking fields.

Apps contain a set of related tabs and objects which are visible to the end user. The below screenshot shows, how the StudentForce app looks like.

salesforce app - salesforce tutorial - edureka

The highlighted portion in the top right corner of the screenshot displays the app name: StudentForce. The text highlighted next to the profile pic is my username: Vardhan NS.

Before you create an object and enter records, you need to set up the skeleton of the app. You can follow the below instructions to set up the app.

Steps To Setup The App

  1. Click on Setup button next to app name in top right corner.
  2. In the bar which is on the left side, go to Build → select Create → select Apps from the drop down menu.
    create salesforce app - salesforce tutorial - edureka
  3. Click on New as shown in the below screenshot.
    new salesforce app - salesforce tutorial - edureka
     
  4. Choose Custom App.
  5. Enter the App Label. StudentForce is the label of my app. Click on Next.custom app - salesforce tutorial - edureka
  6. Choose a profile picture for your app. Click Next.
  7. Choose the tabs you deem necessary. Click Next.
  8. Select the different profiles you want the app to be assigned to. Click Save.

In steps 7 and 8, you were asked to choose the relevant tabs and profiles. Tabs and profiles are an integral part of Salesforce Apps because they help you to manage objects and records in Salesforce.

In this salesforce tutorial, I will give you a detailed explanation of Tabs, Profiles and then show you how to create objects and add records to it.

Salesforce Tabs

Tabs are used to access objects (tables) in the Salesforce App. They appear on top of the screen and are similar to a toolbar. It contains shortcut links to multiple objects. On clicking the object name in a tab, records in that object will be displayed. Tabs also contain links to external web content, custom pages and other URLs. The highlighted portion in the below screenshot is that of Salesforce tabs.

salesforce tabs - salesforce tutorial - edureka

All applications will have a Home tab by default. Standard tabs can be chosen by clicking on ‘+’ in the Tab menu. Accounts, Contacts, Groups, Leads, Profile are the standard tabs offered by Salesforce. For example, Accounts tab will show you the list of accounts in the SFDC org and Contacts tab will show you the list of contacts in the SFDC org.

Steps To Add Tabs

  1. Click on ‘+’ in the tab menu.
  2. Click on Customize tabs, which is present on the right side.
  3. Choose the tabs of your choice and click on Save.custom tabs - salesforce tutorial - edureka

Besides standard tabs, you can also create custom tabs. Students tab that you see in the above screenshot is a custom tab that I have created. This is a shortcut to reach the custom object: Students.

Steps To Create Custom Tabs

  1. Navigate to Setup → Build → Create → Tabs.
  2. Click on New.
  3. Select the object name for which you are creating a tab. In my case, it is Students Data. This is a custom object which I have created (the instructions to create this object is covered later in this blog).
  4. Choose a tab style of your preference and enter a description.
  5. Click on Next → Save. The new Students Data tab will appear as shown below.students data object - salesforce tutorial - edureka

Salesforce Profiles

Every user who needs to access the data or SFDC org will be linked to a profile. A profile is a collection of settings and permissions which controls what a user can view, access and modify in Salesforce.

A profile controls user permissions, object permissions, field permissions, app settings, tab settings, apex class access, Visualforce page access, page layouts, record types, login hour and login IP addresses.

You can define profiles based on the background of the user. For example, different levels of access can be set for different users like system administrator, developer and sales representative.

Similar to tabs, we can use any standard profile or create a custom profile. By default, the available standard profiles are: read only, standard user, marketing user, contract manager, solution manager and system administrator. If you want to create custom profiles, you have to first clone standard profiles and then edit that profile. Do note that one profile can be assigned to many users, but one user cannot be assigned many profiles.

Steps To Create A Profile

  1. Click on Setup → Administer → Manage users → Profiles
  2. You can then clone any of the existing profiles by clicking on Edit.
    salesforce profiles - salesforce tutorial - edureka

Once the tabs and profiles are set up for your App, you can load data into it. The next section of this Salesforce tutorial will thus cover how data is added to objects in the form of records and fields.

Enroll in Salesforce training to fast forward your career.

Objects, Fields And Records In Salesforce

Objects, Fields and Records are the building blocks of Salesforce. So, it is important to know what they are and what role they play in building Apps.

Objects are the database tables in Salesforce where data is stored. There are two types of objects in Salesforce:

  • Standard objects: The objects provided by Salesforce are called standard objects. For example, Accounts, Contacts, Leads, Opportunities, Campaigns, Products, Reports, Dashboard etc.
  • Custom objects: The objects created by users are called custom objects.

Objects are a collection of records and records are a collection of fields.

Every row in an object consists of many fields. Thus a record in an object is a combination of related fields. Look at the below excel for illustration.

sample excel - salesforce tutorial - edureka

I will create an object called Students Data which will contain personal details of students.

Steps to create a custom object:

  1. Navigate to Setup → Build → Create → Object
  2. Click on New Custom Object.
  3. Fill in the Object Name and Description. As you can see from the below image, the object name is Students Data.custom object - salesforce tutotial - edureka
  4. Click on Save.

If you want to add this custom object to the tab menu, then you can follow the instructions mentioned earlier in this Salesforce tutorial blog.

After creating the object, you need to define various fields in that object. e.g. the fields in a student’s record will be student name, student phone number, student email ID, the department a student belongs to and his native city.

You can add records to objects only after defining the fields.

Steps To Add Custom Fields

  1. Navigate to Setup → Build → Create → Objects
  2. Select the object to which you want to add fields. In my case, it is Students Data.
  3. Scroll down to Custom Fields & Relationships for that object and click on New as shown in the below screenshot.new objects - salesforce tutorial - edureka
  4. You need to choose the data type of that particular field and then click Next. I have chosen text format because I will be storing letters in this field.
    The different data types of fields have been explained in detail in the next section of this blog.
  5. You will then be prompted to enter the name of the field, maximum length of that field and description.
  6. You can also make it an optional/ mandatory field and allow/ disallow duplicate values for different records by checking on the check boxes. See the below screenshot to get a better understanding.new fields - salesforce tutotial - edureka
  7. Click on Next.
  8. Select the various profiles who can edit that text field at a later point of time. Click Next.
  9. Select the page layouts that should include this field.
  10. Click Save.

As you can see from the below screenshot, there are two types of fields. Standard fields created for every object by default and Custom fields created by myself. The four fields which I have created for Students Data are City, Department, Email ID and Phone No. You will notice that all custom fields are suffixed with ‘__C’ which indicates that you have the power to edit and delete those fields. Whereas some standard fields can be edited, but not deleted.

fields - salesforce tutotial - edureka

You can now add student records (complete row) to your object.

Steps To Add A Record

  1. Go to the object table from the tab menu. Students Data is the object to which I will add records.
  2. As you can see from the below image, there are no existing records. Click on New to add new student records.new record - salesforce tutotial - edureka
  3. Add student details into different fields as shown in the below screenshot. Click on Save.custom record - salesforce tutotial - edureka
  4. You can create any number of student records. I have created 4 student records as shown in the below screenshot.students - salesforce tutotial - edureka
  5. In case you want to edit the student details, you can click on Edit as shown in the below screenshot.student - salesforce tutotial - edureka

Data Types Of Fields

Data type controls which type of data can be stored in a field. Fields within a record can have different data types. For example:

  • If it is a phone number field, you can choose Phone.
  • If it is a name or a text field, you can choose Text.
  • If it is a date/ time field, you can choose Date/Time.
  • By choosing Picklist as data type for a field, you can write predefined values in that field and create a drop-down.

You can choose any one of the data types for custom fields. Below is a screenshot listing the different data types.

data types - salesforce tutorial - edureka

Data types like Lookup Relationship, Master-Detail Relationship and External Lookup Relationship are used to create links/ relationships between one or more objects. Relationships between objects is the next topic of discussion in this Salesforce tutorial blog.

Object Relationship In Salesforce

As the name suggests, object relationship is used in Salesforce to create a link between two objects. The question on your mind would be, why is it needed? Let me talk about the need with an example.

In my StudentForce app, there is a Students Data object, which contains personal information of students. Details regarding student’s marks and their previous college are present in different objects. We can use relationships to link these objects using related fields. The marks of the students and colleges can be linked with the Student Name field of Student Data object.

Relationships can be defined while choosing the data type. They are always defined in the child object and are referenced to the common field in master object. Creating such links will help you to search and query data easily when the required data is present in different objects. There are three different types of relationships that can exist between objects. They are:

  • Master-Detail
  • Lookup
  • Junction

Let us look into each of them:

Find out our Salesforce Training Course in Top Cities

IndiaUnited StatesOther Countries
Salesforce Training in HyderabadSalesforce Training in DallasSalesforce Training in Melbourne
Salesforce Training in BangaloreSalesforce Training in CharlotteSalesforce Training in London
Salesforce Training in ChennaiSalesforce Training in NYCSalesforce Training in Sydney

Master-Detail Relationship (1:n)

Master-Detail relationship is a parent-child relationship in which the master object controls the behaviour of the dependent object. It is a 1:n relationship, in which there can be only one parent, but many children. In my example, Students Data is the master object and Marks is the child object.

Let me give you an example of a Master-Detail relationship. The Students Data object contains student records. Each record contains personal information about a student. However, the marks obtained by students are present in another record called Marks. Look at the screenshot of Marks object below.

master detail relationship - salesforce tutotial - edureka

I have created a link between these two objects by using the student’s name. Below are the points you have to keep in mind when setting up a Master-Detail relationship.

  • Being the controlling object, the master field cannot be empty.
  • If a record/ field in master object is deleted, the corresponding fields in the dependent object are also deleted. This is called a cascade delete.
  • Dependent fields will inherit the owner, sharing and security settings from its master.

You can define master-detail relationships between two custom objects, or between a custom object and standard object as long as the standard object is the master in the relationship.

Lookup Relationship (1:n)

Lookup relationships are used when you want to create a link between two objects, but without the dependency on the parent object. You can think of this as a form of parent-child relationship where there is only one parent, but many children i.e. 1:n relationship. Below are the points you have to keep in mind when setting up a Lookup relationship.

  • The lookup field on the child object is not necessarily required.
  • The fields/ records in a child object cannot be deleted by deleting a record in the parent object. Thus the records in the child object will not be affected.
  • The child fields will not inherit the owner, sharing and security settings of its parent.

An example of a lookup relationship in my case would be that of a College object. You can see the child object: Students Data in the below screenshot. You will notice that there is an empty College field for the first record. This indicates that the dependency is not a necessity.

lookup relationship - salesforce tutotial - edureka

Below is a screenshot of the schema diagram of both the relationships. College – Student Data forms the Lookup relationship and Student Data – Marks forms the Master-Detail relationship.

schema builder 1 - salesforce tutotial - edureka

Self-Relationship

This is a form of lookup relationship where instead of two tables/ objects, the relationship is within the same table/ object. Hence the name self-relationship. Here, the lookup is referenced to the same table. This relationship is also called Hierarchical relationship.

Junction Relationship (Many-To-Many)

This kind of a relationship can exist when there is a need to create two master-detail relationships. Two master-detail relationships can be created by linking 3 custom objects. Here, two objects will be master objects and the third object will be dependent on both the objects. In simpler words, it will be a child object for both the master objects.

To give you an example of this relationship, I have created two new objects.

  • A master object called Professor. It contains the list of professors.
  • A child object called Courses. It contains the list of courses available.
  • I will use the Students Data object as another master object.

I have created a many-to-many relationship such that every record in the Courses object must have at least one student and at least one professor. This is because every course is a combination of students and professors. In fact, a course, can have one or more number of students and professors associated with them.

The dependency on Student and Professor objects makes Courses as the child object. Student and Professor are thus the master objects. Below is a screenshot of Courses object.

many to many relationship - salesforce tutotial - edureka

You will notice that there are different combinations of professors and students for these subjects. For example, Kate is associated with two courses and has two different professors for each of those two courses. Mike is associated with only one course, but, has two different professors for that course. Both Joe and Kate are associated with the same course and same professor. In the below screenshot, you will find the schematic diagram of this relationship.

schema builder 2 - salesforce tutotial - edureka

Congrats! The StudentForce App is successfully built. The two schema diagrams present above show how the different objects are linked inside my Salesforce App.

This brings us to the end of this Salesforce tutorial. I hope you understood the various concepts like apps, tabs, profiles, fields, objects and relationships which were explained in this Salesforce tutorial blog. In case you have any doubts or queries, feel free to leave them in the comment section below and I will get back to you at the earliest.

I urge you to see this Salesforce tutorial video  that explains the creation of Salesforce student app. Go ahead, enjoy the video and tell me what you think.

Salesforce Tutorial For Beginners | Learn To Create Salesforce App | Salesforce Training | Edureka

This Salesforce Tutorial video will help you learn how to create a Salesforce app from scratch. This is a step by step tutorial on creating Salesforce app and ideal for beginners.

Stay tuned to read the next blog in our Salesforce tutorial series. In the meantime, I would suggest you to create a Salesforce account and play around with the Salesforce app. You can try building your own app by following the instructions mentioned above.

If you want to become a professional skilled in Salesforce then, check out our Salesforce Training in Columbus which comes with instructor-led live training and real life project experience.

Original article source at: https://www.edureka.co/

#salesforce #app 

Learn To Create Your Own Salesforce App
Roscoe  Batz

Roscoe Batz

1669173911

What Is VueJS? | A VueJS Tutorial

In this VueJS article, we will learn together what is VueJS? | VueJS Tutorial. VueJS is an open-source JavaScript framework that is typically used to develop interactive interfaces. From an MVC point of view, while developing with VueJS, it focuses on the view part. What makes VueJS stand apart is its beginner-friendly learning curve and ability to easily integrate with any web project and libraries. 

A very popular framework that is experiencing tremendous growth (in 2019 alone, the downloads have doubled) being free, tiny and very informant. A driving force for its rise has most certainly been the growth of the popular PHP framework Laravel that adopts Vue functionalities.

Do you know what the word vue means? Vue stands for sight or vision (not a Scrabble word, though). So to broaden our “vision” and to dig deeper into this new JavaScript framework world, we may need to know what sets VueJS apart and why you should take up learning something new. 

Now before getting into the technicalities, one may want to know more about why you should learn VueJS and what it offers in contrast to similar JavaScript frameworks. So let’s dive into that.

Why VueJS? 

VueJS is known for being a progressive framework. That means whether you are a complete beginner to this JavaScript framework or an experienced developer who is simply transitioning, VueJS adapts to your needs. It is simple, tiny (~24KB) and mostly performance-oriented. Being lightweight, it definitely feels different from all the other JavaScript front-end frameworks and view libraries and offers great performance. 

While other frameworks require a complete makeover while implementing new JavaScript frameworks because they come with their own exclusive set of conventions, Vue can effortlessly glide into your project by a mere <script> tag. You heard that, right! This is a great pro considering the growing demand of JavaScript front-end developers and the existing, complex frameworks perplexing both debutant devs and veteran ones alike. You don’t need to be overwhelmed with complex words like npm, Babel etc., to get started with Vue. 

If you are a Laravel developer, you are in for a stroke of luck because learning VueJS would be a breeze. Don’t fret even if you’re already adept in ReactJS or AngularJS. VueJS stands on a pedestal for being the best of all worlds, taking the essential parts of other popular frameworks and simply enhancing from there.

VueJS vs. Angular:

With Angular, implementing TypeScript is mandatory. As not everyone enjoys TypeScript is a polarizing decision by the authors of Angular. Vue decided on not making TypeScript compulsory.

Vue is less opinionated than Angular. This means there is no one-way of writing code in vue.js. Software developers can be more inventive and use vue.js in the manner they want to. As a result, quick wins are faster to achieve, and the learning curve of vue.js is a lot less steep compared to the learning curve of Angular.

Angular directives are typically more complex than vue.js directives. Vue differentiates directives and components more lucidly.

Vue.js directives encapsulate DOM manipulations only, whereas Angular directives are able to satisfy a wider variety of use cases.

Both Angular and Vue.js are feasible choices for web application development. They are also alike in nature when it comes to writing code. Vue.js is more lightweight, while Angular is much more enterprise-ready for generating compact applications.

VueJS vs. React:

React assumes the JSX format, where HTML is written in JavaScript. Vue is easier to adopt compared to React. Vue separates concerns in a manner that web developers are already used to, dealing with HTML, CSS, and JavaScript. It also permits the use of JSX for developers who are already familiar with its workings.

React, and Vue performs the same manipulations within an almost similar timeframe. The variation between the performance efficiency for Vue and React is almost negligible, as it just varies by a few milliseconds. React offers versatility, a very much in-demand job market, and is as well-built as Vue. Yet Vue is more structured, easier to figure out and set up.

Reasons to learn VueJS 

Developers love building web applications using Vue.js, here’s why –

  1. No complex building application required
     

A critical aspect while developing vue was keeping it simple. It’s not necessary for a building tool to develop projects using VueJS. Even the installation for VueJS is very easy, to begin with. In a matter of time, any developer can clearly understand and produce interactive web interfaces. One can do so by using the HTML <script> tag. Just include the src tag pointing to the latest development or production version and you are good to go! 

  1. Easy to use a command-line interface

Vue command-line interface gives a plethora of useful supplementary features, including an interactive project initialization wizard (available through the terminal or a web-based UI), a plugin system to maintain generators and configuration for community add-ons, and the capacity to define alternative build targets, like web components or as libraries.

  1. Robust Environment

Vue provides standard support for several inherent add-ons, including vue-router for client-side routing, the vue-devtools browser extension for debugging, vuex for managing state, vue-test-utils for unit testing components, Vue CLI as specified earlier. 

  1. An interactive and ever-growing community 

The Vue forums are a magnificent way to get insight from specialists on complicated issues. Vue conferences are also a great way to reach out to other members of the community and attend more in-depth workshops that are already taking place worldwide. Vue has a distributed, hard-working core unit, who are continually enhancing the framework without over-burdening developers with a bunch of painful upgrades.

  1. The Vue instance

A vue instance invocation is how every Vue application is initialised. It also produces a fully-reactive root component of the application. Vue’s reactivity system is impressive in its simplicity.

  1. Beginner-friendly learning curve

Vue is easy to learn. Developers do not have to go through an ocean of plug-ins and setups to get started with Vue. It’s mostly pretty straightforward. If you’re adept with HTML, CSS and JavaScript, you’re raring to go.

Installing Vue

  1. Using the <script> tag in HTML file

As mentioned above, one of the easiest ways to use Vue is by directly implementing it via HTML with the <script> tag. Simply download Vue (from the home site https://vuejs.org/v2/guide/installation.html) and include with a script tag. Vue will be registered as a global variable. 

There are two versions available. Use the development version if you don’t want to miss out on basic warnings and avoiding common mistakes. 

  1. Using NPM

NPM is the suggested installation method when developing large scale applications with Vue. It comes with Browserify and Webpack along with other necessary tools, which help with more straightforward development. Following is the command to install using npm.

npm  install vue

  1. Using Command Line Interface

The Vue CLI is a command line utility, and you install it globally using npm. The CLI is important for rapid Vue.js development.

npm install -g @vue/cli

or using Yarn:

yarn global add @vue/cli

Once you do so, you can invoke the vue command as below –

VueJS – Instance

To start with VueJS, we need to create the instance of Vue, which is called the root Vue instance. A Vue application consists of a root Vue instance created with new Vue, optionally organized into a tree of nested, reusable components. 

Syntax:

1

2

3

var instance = new Vue({

   // options

})

Example

HTML –

1

2

3

4

5

6

7

8

9

10

11

12

13

14

<html>

   <head>

    <title>VueJs Instance</title>

    <script type = "text/javascript" src = "js/vue.js"></script>

   </head>

   <body>

    <div id = "vue_instance">

        <h1>Firstname : {{firstname}}</h1>

        <h1>Lastname : {{lastname}}</h1>

        <h1>{{mydetails()}}</h1>

    </div>

    <script type = "text/javascript" src = "js/vue_instance.js"></script>

   </body>

</html>

VueJS –

1

2

3

4

5

6

7

8

9

10

11

12

13

var  vm = new Vue({

   el: '#vue_instance',

   data: {

      firstname : "John",

      lastname  : "Doe",

      subject    : " GreatLearning VueJS Tutorial"

   },

   methods: {

      mydetails : function() {

         return "I am "+this.firstname +" "+ this.lastname + " studying " + this.subject;

      }

   }

})

In VueJS, there is a parameter that we have used called el. It takes the id of the DOM element. In the above example, we have the id #vue_instance. It is the id of the div tag, that is present in HTML.

1<div id = "vue_instance"></div>

Here, whatever we are going to do will only have an effect on the div element and nothing outside it. Next, we have defined the data object. It has a value firstname, lastname, and address.

The same is denoted inside the div. For example,

1

2

3

4

5

<div id = "vue_instance">

        <h1>Firstname : {{firstname}}</h1>

        <h1>Lastname : {{lastname}}</h1>

        <h1>{{mydetails()}}</h1>

  </div>

The Firstname : {{firstname}} value will be replaced inside the interpolation, i.e. {{}} with the value assigned in the data object, i.e. John. The same goes for last name.

Furthermore, we have methods where we have written a function mydetails and a return value. It is assigned inside the div as:

1<h1>{{mydetails()}}</h1>

Therefore, inside {{} } the function mydetails is called. The value that is returned by the Vue instance will be shown to usinside {{}}. Check the output for reference.

VueJS – Component

Vue Components are one of the most essential features of VueJS that provides custom elements that can be used repeatedly in HTML. Components are individual, detached units of an interface. They can have their own state, markup and behaviour.

There are four ways in which you can define a component –

  • Vue instance

1

2

3

new Vue({

  /* options */

})

  • Using the .component directive

1

2

3

Vue.component('component-name', {

  /* options */

})

  • Declaring a local component i.e components that are accessible only within itself and not available to be used anywhere else (encapsulation property)
  • Using .vue extension in a file

Example –

1

2

3

4

5

6

7

8

new Vue({                   //Vue instance using el ‘element’

  el: '#app'

})

Vue.component('user-name', {    //.component global directive

  props: ['name'],

  template: '<p>Hi {{ name }}</p>'

})

The building blocks of a component 

So far we’ve discussed how a component can accept the el and props properties.

  • el is only utilised in root components initialized using new Vue({}), and checks the DOM element the component will mount on.
  • props enlist all the properties that we can pass down to a child component

A component also allows other properties:

  • Data –  the component local states
  • Methods –  the component methods
  • Watch – the component watchers (next topic)

Advantages of components 

Computed properties are well known for handling use cases like:

  • Acquiring a new data value from existing (e.g. fullName = firstName + lastName)
  • Extracting a complicated JS expression into a clearer property
  • You require a property to react have greater than one data dependency

Thus with components, we can ensure better readability and maintainability of JavaScript code.

Reusability of Components 

A child component can be added multiple times. Each separate instance is independent of the others:

HTML  –

1

2

3

4

5

<div id="app">

  <user-name name="Anthony"></user-name>

  <user-name name="Akbar"></user-name>   

  <user-name name="Amar"></user-name>   <!-- reuse component-->

</div>

Vue –

1

2

3

4

5

6

7

8

Vue.component('user-name', {

  props: ['name'],

  template: '<p> {{ name }} is learning VueJS. </p>'

})

new Vue({

  el: '#app'

})

Below we can see the output with the reusability of the component in multiple div tags of HTML. 

VueJS – Computed Properties

A Vue method is a function concomitant with the Vue instance. Methods are defined inside the methods property. Methods are mainly beneficial when you need to execute an action, and you attach a v-on directive on an element to handle events. Computed properties are somewhat like methods but with some difference in comparison to methods in terms of behaviour. However, the difference is that computed properties are cached centred on their reactive dependencies. A computed property will only re-evaluate when some of its reactive dependencies have transformed. 

Why do we need caching? Imagine we have a performance-extensive computed property A, which calls for looping through a huge array and doing many computations. Then we may have additional computed properties that, in turn, are dependent on A. Without caching, we would be executing A’s getter many more times than needed. In cases where caching is not required, use a method as an alternative.

Example:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

<html>

   <head>

    <title>VueJs Instance</title>

    <script type = "text/javascript" src = "js/vue.js"></script>

   </head>

   <body>

    <div id = "computed_props">

        City : <input type = "text" v-model = "city" /> <br/><br/>

        Country : <input type = "text" v-model = "country"/> <br/><br/>

      

        <h1>Using computed method : {{getaddress}}</h1>

    </div>

    <script type = "text/javascript" src = "js/vue_computedprops.js"></script>

   </body>

</html>

vue_computeprops.js

1

2

3

4

5

6

7

8

9

10

11

12

13

var vm = new Vue({

   el: '#computed_props',

   data: {

    city :"",           //computed property

    country :"",        //computed property

      

   },

   computed :{

    getaddress : function(){

        return this.city +" is in "+ this.country;

    }

   }

})

Here, we have created an HTML file with city and country. city and country is a textbox that is bound using properties city and country.

When we type in the textbox the same is returned by the function, then the properties of the city or country are changed. Therefore, with the help of computed properties, we don’t have to do anything specific, such as recalling to call a function. With computed properties, it gets called by itself, as the properties used inside the textbox changes, i.e. city and country.

VueJS – Watch Property

A Vue watch property observes a specific attribute and is able to identify when that attribute changes. It basically acts as an event listener to a specific data attribute i.e it allows you to perform a function on the property as and when it changes.

The only thing you need to think about while using the watch property is a feature you want to track. It’s as simple as that!

Format:

1

2

3

4

5

6

7

8

9

10

11

export default {

    data () {

        return {

            title: '',

            description: ''

        }

    },

    watch: {

        // the things you want to track go here!

    }

}

Watchers can heavily be relied upon while one needs to observe data until it reaches a specific value and to make asynchronous API calls when values change. The function assigned to watch (below name) can be assigned at most 2 parameters – the first being new and the second being the old.

1

2

3

4

5

watch: {

    city: (newCity, oldCity) => {

        console.log("City changed from " + oldCity+ " to " + newCity)

    }

}

Since you have already read up on VueJS components, you can think of watchers as an extended and more generalised form of it. Primarily, if you need to perform an action based on the change of an attribute, watchers are your saving grace. However, watchers can be quite redundant in their use-case if you see the example below –

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

new Vue({

    el: '#app',

    data: {

     km2: 0,

        cm: 0,

        mm: 0

    },

    watch: {

        km2: function(val){

            this.km2 = val;

            this.cm = val * 100000;

            this.mm = val * 1000000;

        },

        cm: function(val){

            this.km2 = val / 100000;

            this.cm = val;

            this.mm = val * 10;

        },

        mm: function(val){

            this.km2 = val / 1000000;

            this.cm = val / 10;

            this.mm = val;

        },

    }

});

As you can see, we have to define the conversion properties n times for n attributes i.e n2 declarations. Thus it is often a better alternative to use computed properties instead of watchers.

Example – 

Code:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

<html>

   <head>

      <title>VueJs Instance</title>

      <script type = "text/javascript" src = "js/vue.js"></script>

   </head>

   <body>

      <div id = "computed_props">

        Celsius : <input type = "text" v-model = "celsius">

        <br> <br>

        Fahrenheit : <input type = "text" v-model = "fahrenheit">

    </div>

      <script type = "text/javascript">

          var vm = new Vue({

            el: '#computed_props',

            data: {

            celsius : 0,

            fahrenheit: 0

            },

            methods: {

            },

            computed :{

            },

            watch : {

            celsius:function(val) {

                this.celsius = val;

                this.fahrenheit = val*1.8 + 32;             },

            fahrenheit : function (val) {

                this.celsius = (val-32)/1.8;

                this.fahrenheit = val;

            }

            }

        });

      </script>

   </body>

</html>

 

VueJS – Binding

VueJS offers an excellent feature of binding HTML attributes in order to manipulate the class or its style. In order to do this, we use a vue.js directive called the ‘v-bind’. It not only modifies strings but can also help in modifying objects and arrays.

Syntax – 

1<a v-bind:href="url">{{ linkText }}</a>

V-bind is used so frequently that there also exists an alternative syntax –

1

2

<a v-bind:href="url">{{ linkText }}</a>

<a :href="url">{{ linkText }}</a>

v-bind can be used for the following purposes – 

  1. Binding HTML classes
  2. Binding in-line styles
  3. Form input bindings

Example 
HTML:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

<html>

   <head>

    <title>VueJs Instance</title>

    <script type = "text/javascript" src = "js/vue.js"></script>

   </head>

   <body>

    <style>

        .active {

            background: yellow;

        }

    </style>

    <div id = "classbinding">     //HTML Class binding

        <div v-bind:class = "{active:isactive}"><b>{{title}}</b></div>

    </div>

        <div id = "databinding">      //Inline CSS binding

        <div v-bind:style = "{ color: activeColor, fontSize: fontSize + 'px' }">{{title}}</div>

    </div>

    <div id = "formbinding">      //Form element binding

        <h3>Radio</h3>

        <input type = "radio" id = "black" value = "Black" v-model = "picked">Black

        <input type = "radio" id = "white" value = "White" v-model = "picked">White

        <h3>Radio element clicked : {{picked}} </h3>

        <hr/>

        <h3>Select</h3>

        <select v-model = "languages">

            <option disabled value = "">Please select one</option>

            <option>Angular</option>

            <option>React</option>

            <option>Vue</option>

            <option>Vanilla</option>

        </select>

        <h3>Framework Selected is : {{ languages }}</h3>

        <hr/>

    </div>

     

   </body>

</html>

Vue.js –

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

var vm = new Vue({

            el: '#classbinding',

            data: {

            title : "HTML BINDING",

            isactive : true

            }

        });

          

 var vm = new Vue({

            el: '#databinding',

            data: {

            title : "Inline Style Binding",

            activeColor: 'blue',

            fontSize :'30'

            }

        }); 

          

 var vm = new Vue({

            el: '#formbinding',

            data: {

            picked : 'White',

            languages : "Vanilla"

            }

        });        

VueJS – Events 

When you develop a dynamic website using Vue you’ll most likely want it to be able to respond to some actions or events. For example, if a user glides over an image, submits a form, or even clicks a button, you may want your Vue site to respond somehow.

When you think of event handling in VueJS, remember v-on. v-on is the property added to the DOM elements to listen to the events in VueJS and trigger some action. We add an argument to the v-on element, here click which is the name of the event to be handled. We also bind an expression to the event, here clickEvent.

<button v-on:click=”clickEvent”></button>

Event handling also involves web and mobile native events. Other events that can be handled using v-on are :

  • submit
  • keyup
  • drag
  • scroll

Event Modifiers 

A modifier can be added on v-on property. A dot operator is required to be added while dealing with modifiers. Vue offers a myriad of different event modifiers that are essential in common event handling scenarios:

  • .stop – event propagation will be stopped
  • .prevent – Prevents default behaviour
  • .self – Target of the event is itself
  • .once – Run the function at most once

Eg. Instead of 

1

2

3

4

5

6

7

8

formHandler (event) {

    event.preventDefault();

    // form handling logic

}

 Use       

 <form @submit.prevent="formHandler"></form>

.once event modifier example code

HTML – 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

<html>

  <head>

    <title>VueJs Instance</title>

    <script type="text/javascript" src="js/vue.js"></script>

  </head>

  <body>

    <div id="databinding">

    <button v-on:click.once="clickedonce"

v-bind:style="styleobj">Click Once</button> <! .once modifier -->

  You can only click {{var1}} time.     <br /><br />

    <button v-on:click="clickedmultiple"

v-bind:style="styleobj">Click Me</button> <!-- no event modifier -->

    You can click {{var2}} times!

    </div>

 </body>

</html>

VueJS –

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

var vm = new Vue({

            el: '#databinding'

            data: {

            clicknum : 0,

            clicknum1 :0,

            styleobj: {

                backgroundColor: '#2196F3!important',

                cursor: 'pointer',

                padding: '8px 16px',

                verticalAlign: 'middle',

            }

            },

            methods : {

            clickedonce : function() {

                this.var1++;  //since .once is used, this won’t be added after value 1 is reached

            },

            clickedmultiple : function() {

                this.var2++; //this gets added as usual

            }

            }

        });

In the given example, we have created two buttons. The button with Click Once label has been provided with the .once modifier and the other button is without any modifier. Thus we can see the working of the event modifier in this manner.

1

2

<button v-on:click.once = "buttonclickedonce" v-bind:style = "styleobj">Click Once</button>

<button v-on:click = "buttonclicked"  v-bind:style = "styleobj">Click Me</button>

The former button calls the method “clickedonce” and the latter button calls the method “clickedmultiple”.

1

2

3

4

5

6

clickedonce : function() {

   this.clicknum++;

},

clickedmultiple: function() {

   this.clicknum1++;

}

There are two variables defined in var1 and var2. Both are incremented when the button is clicked. Both the variables are initialized to 0 and the display is seen in the output above.

When we click the former button, the variable var1 increments by 1. On the second click, the number is not incremented as the .once event modifier prevents it from executing or performing any action item assigned on the click of the button.

On the click of the latter button, the same action is carried out, i.e. the variable is incremented. On every click, the value is incremented and displayed.

The output we get in the browser is as shown below –

Key Modifiers 

Just like we have event modifiers, VueJS offers key modifiers based on which we can control the event using input keys. Consider we have a textbox and we want to submit only when we press Enter. We can do so by denoting the key modifier to the submit function as below.

1

2

<!-- only call `vm.submit()` when the `key` is `Enter` -->

<input v-on:keyup.enter="submit">

Vue contains aliases for the most commonly used key codes when necessary for browser support:

  • .enter
  • .tab
  • .delete (captures both “Delete” and “Backspace” keys)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right

Example –

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

<html>

   <head>

    <title>VueJs Instance</title>

    <script type = "text/javascript" src = "js/vue.js"></script>

   </head>

   <body>

    <div id = "databinding">

        <input type = "text" v-on:keyup.enter = "showinputvalue" v-bind:style =

"styleobj" placeholder = "Value you want to enter/> <!-- key modifier .enter used -->

        <h3> {{name}}</h3>

    </div>

    <script type = "text/javascript">

        var vm = new Vue({

            el: '#databinding',

            data: {

            Name:'',

            styleobj: {

                width: "30%",

                padding: "12px 20px",

                margin: "8px 0",

                boxSizing: "border-box"

            }

            },

            methods : {

            showinputvalue : function(event) {

                this.name=event.target.value + “ was entered.”;

            }

            }

        });

    </script>

   </body>

</html>

Type anything into the box and press enter to view, as you can see below.

Custom Events 

All that was mentioned above were in-built, native events. VueJS being a component-based framework also allows for custom events. We have learnt in Vue that parents can send prop data to a child, but not the other way round. This can only be achieved through custom events – the child component emits the event to the parent who listens to it.

Example –

HTML

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

<html>

   <head>

      <title>VueJs Instance</title>

      <script type = "text/javascript" src = "js/vue.js"></script>

   </head>

   <body>

      <div id = "databinding">

         <div id = "counter-event-example">

            <p style = "font-size:25px;">VueJS Feature selected : <b>{{ featureclicked }}</b></p>

            <button-counter      <-- parent component -->

            v-for = "(item, index) in features"

            v-bind:item = "item"

            v-bind:index = "index"

            v-on:showfeature = "featuredisp">

        </button-counter>

         </div>

      </div>

     

   </body>

</html>

Vue.js

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

Vue.component('button-counter', {

            template: '<button v-on:click = "displayfeature(item)"><span style = "font-size:25px;">{{ item }}</span></button>',

            data: function () {

            return {

                counter: 0

            }

            },

            props:['item'],

            methods: {

            displayfeature: function (ftr) {

                console.log(lng);

                this.$emit('showfeature', ftr);

            }

            },

        });

        var vm = new Vue({

            el: '#databinding',

            data: {

            featureclicked: "",

            features : ["Instance", "Component", "Watchers", "Directives", "Binding", "Animation", "Rendering", "Routing"]

            },

            methods: {

            featuredisp: function (a) {

                this.featureclicked = a;

            }

            }

        })

The name of the custom event is showfeature and it calls a method called featuredisp which is defined in the Vue instance. The method displayfeature calls this.$emit(‘showfeature’, ftr);

$emit is used to call the parent component method. The method showfeature is the event name given on the component with v-on. Here, the emit triggers showfeature which in turn calls featuredisp from the Vue instance methods. It assigns the feature clicked to the variable featureclicked and the same is displayed in the browser as shown in the following screenshot.

VueJS – Rendering

VueJS offers rendering in two widely used formats :

Conditional Rendering 

Here we want the output to be displayed or method to be performed only when a certain condition is met. Anyone and everyone who is even a novice developer knows that while dealing with conditions, we use if, else and else if etc. The very same goes for VueJS. The tags used are – 

  • v-if
  • v-else
  • v-else-if

Some things to note in conditional rendering –

  1. The block can be rendered only if the condition is met.
  2. V-else requires the use of at least one v-if block and should be written immediately after v-if in order to be recognised.
  3. The above is also true for v-else-if i.e it should follow a v-if statement. V-else-if can be rendered for multiple conditions.

1

2

3

4

5

6

7

8

9

10

11

12

<div v-if="type === 'A'">

  A

</div>

<div v-else-if="type === 'B'">

  B

</div>

<div v-else-if="type === 'C'">

  C

</div>

<div v-else>

  Not A/B/C

</div>

v-show

Another substitute for conditionally representing an element is the v-show directive. The usage is largely the same:

1<h1 v-show="ok">Hello World!</h1>

With v-show, the difference is that an element will be rendered every time and remain in the DOM; v-show only toggles the display CSS property of the element. Note that v-show doesn’t support the <template> element and also does not work with v-else.

Difference between v-if and v-show

V-if and its associates stand for “real” conditional rendering because it assures that event listeners and child elements inside the conditional block are properly terminated and re-created. In comparison, v-show is much more manageable – the element is always rendered regardless of the initial condition, with CSS-based toggling. So favour v-show if you need to toggle something very frequently, and prefer v-if if the condition is unlikely to vary at runtime.

Example –

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

<html>

   <head>

    <title>VueJs Instance</title>

    <script type = "text/javascript" src = "js/vue.js"></script>

   </head>

   <body>

    <div id = "databinding">

        <button v-on:click = "showdata" v-bind:style = "styleobj">Click Me</button>

        <span style = "font-size:25px;"><b>{{show}}</b></span>

        <h1 v-if = "show">This is h1 if </h1> <!-- if condn -->

        <h2 v-else>This is h2 else </h2>        <!-- else condn -->

        <h3 v-show = "show">This is h3 show </h3>   <!-- show condition -->

         </div>

    <script type = "text/javascript">

      var vm = new Vue({

            el: '#databinding',

        data: {

            show: true,

            styleobj: {

                backgroundColor: '#2196F3!important',

                cursor: 'pointer',

                padding: '8px 16px',

                verticalAlign: 'middle',

            }

            },

            methods : {

            showdata : function() {

                this.show = !this.show;

            }

            },

        });

    </script>

   </body>

</html>

As you can see by the above output, when the result on clicking the button is true, we can see the contents of the v-if and v-show tag. And on value false, we only see the content of the v-else tag. You can easily infer from this what we have already discussed, v-if needs to be accompanied by a v-else tag and v-show can work independently. 

List Rendering 

In case of managing arrays and other such objects dynamically and consistently, we use the list rendering property offered by VueJS. 

One can utilise the v-for directive to render a list of items based on an array. The v-for directive asks for a specialised syntax in the form of item in a list of items, where items is the source data array and item is an alias for the array element being rendered on:

HTML –

1

2

3

4

5

<ul id="example-1">

  <li v-for="item in items" :key="item.message">

    {{ item.message }}

  </li>

</ul>

VueJS – 

1

2

3

4

5

6

7

8

9

var example1 = new Vue({

  el: '#example-1',

  data: {

    items: [

      { message: 'Amar' },

      { message: 'Akbar' }

    ]

  }

})

Result:

We have complete access to parent scope properties inside v-for blocks. It also gives support to an optional second argument for the index of the present item. One can also use of as a delimiter instead of in, making it closer to JavaScript’s syntax for iterators. 

1<div v-for="item of items"></div>

Example –

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

<html>

   <head>

    <title>VueJs Instance</title>

    <script type = "text/javascript" src = "js/vue.js"></script>

   </head>

   <body>

    <div id = "databinding">

        <input type = "text" v-on:keyup.enter = "showinputvalue"

            v-bind:style = "styleobj" placeholder = "Enter Countries"/>  <!-- v-for triggered with key event .enter -->

        <h1 v-if = "items.length>0">Display Countries</h1>

        <ul>

            <li v-for = "a in items">{{a}}</li>

        </ul>   <!-- v-for rendered as a list item -->

    </div>

    <script type = "text/javascript">

        var vm = new Vue({

            el: '#databinding',

            data: {

            items:[],

            styleobj: {

                width: "30%",

                padding: "12px 20px",

                margin: "8px 0",

                boxSizing: "border-box"

            }

            },

            methods : {

            showinputvalue : function(event) {

                this.items.push(event.target.value);

            }

            },

        });

    </script>

   </body>

</html>

VueJS – Mixins 

Mixins are a piece of predefined code that can be used across Vue components and instances in order to extend functionalities. Vue Mixins can be shared amongst components without the need to repeat logic. Mixins are a flexible way to reuse functionalities of Vue elements. A mixin object can contain any component alternatives. A component using a mixin has all options “incorporated” into the component’s own options.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

// define a mixin object

var myMixin = {

  created: function () {

    this.hello()

  },

  methods: {

    hello: function () {

      console.log('hello from mixin!')

    }

  }

}

// define a component that uses this mixin

var Component = Vue.extend({

  mixins: [myMixin]

})

var component = new Component() // => "hello from mixin!"

Types of Mixins

Following are the types of mixins that can be declared in Vue:

  1. Local Mixins: This is the type that is mentioned above. It is scoped to the element it is imported into and expressed in. The capabilities of the local mixin are bound by the component it is imported in.
     
  2. Global Mixins: It is a modified type of mixin that is generally defined in the Main.js file of any Vue project. The Vue team advises that it is to be used with caution as it affects all Vue components in an application. The syntax of a global mixin looks like this:

1

2

3

4

5

6

7

8

9

10

11

12

13

Vue.mixin({

  created: function () {

    var myOption = this.$options.myOption

     if (myOption) {

        console.log(myOption)

        }

    }

})

new Vue({

        myOption: 'hello!'

    })

    // => "hello!"

So why exactly do we need mix-ins?

  1. One can easily adhere to the DRY principle in programming with Vue mixins,  which is simply ensuring that you do not repeat yourself.
  2. Flexibility of code can easily be attained with the help of  Vue mixins.
  3. Vue mixins are also safe, they do not affect changes outside their defined scope if they are well written.
  4. They are a great platform for code reusability.

Example:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

<html>

   <head>

    <title>VueJs Instance</title>

    <script type = "text/javascript" src = "js/vue.js"></script>

   </head>

   <body>

    <div id = "databinding"></div>

    <script type = "text/javascript">

      var vm = new Vue({

            el: '#databinding',

            data: {

            },

            methods : {

            },

        });

        var myMixin = {

            created: function () {

            this.startmixin()

            },

            methods: {

            startmixin: function () {

                alert("Mixin Popup!");

            }

            }

        };

        var Component = Vue.extend({

            mixins: [myMixin]

        })

        var component = new Component();

    </script>

   </body>

</html>

VueJS- Reactive Interface 

Vue reactive interface is one of the essential features of the Vue.js framework. Vue.js offers the possibility to utilise the reactivity interface on the features that are dynamically added. Here, we are going to generate a Vue Instance, and then attach a watch property to the Vue instance.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

import Vue from 'vue'

import App from './App'

import router from './router'

Vue.config.productionTip = false

var vm = new Vue({

   el: '#app',

   data: {

      items: 0

   }

});

vm.$watch('items', function(nval, oval) {

   alert('Item is incremented :' + oval + ' to ' + nval + '!');

});

setTimeout(

   function(){

      vm.items = 25;

   },2500

);

The above reactive property increments items based on button clicks (even the Alert dialog box ‘OK’ button). Vue.js cannot detect the property of addition. The best way to categorize the property is always to define the properties, which are needed to be reactive upfront in the Vue instance. There are several methods of the Vue reactivity Interface, which are very necessary in the Vue.js application. Some of the most popular methods are as follows:

vue.set

The vue.set method is used to set an attribute on an object. Vue.js cannot identify the addition or deletion property in the Vue component. This is due to the fact that Vue conducts the conversion process of getter/setter during the initialisation of instances. A getter/setter property must be at hand in the object of data in order for Vue to convert it and make it reactive. The vue.set method works according to the following syntax : 

Syntax:

1vue.set( target, key, value )

Here the target, key, and value denote for: 

Target: It could be an object/ an array.
Key: It could be a string/ number.
Value: It could be of any type.

Example :

HTML –

1

2

3

4

5

6

7

8

9

10

11

12

13

<html>

   <head>

    <title>VueJs Instance</title>

    <script type = "text/javascript" src = "js/vue.js"></script>

   </head>

   <body>

    <div id = "app">

        <p style = "font-size:25px;">Total items: {{ items.id }}</p>

        <button @click = "items.id++" style = "font-size:25px;">Add item</button>

    </div>

      

   </body>

</html>

Vue.js –

1

2

3

4

5

6

7

8

9

10

11

12

13

var myitem = {"id":1, name:"item", "price":"20.00"};

        var vm = new Vue({

            el: '#app',

            data: {

            counter: 1,

            items: myitem

            }

        });

        Vue.set(myitem, 'qty', 1);

        console.log(vm);

        vm.$watch('total', function(nval, oval) {

            alert('Total items are incremented :' + oval + ' to ' + nval + '!');

        });

vue.delete 

The Vue.delete method is used to dynamically delete the property. We can have the following syntax to use the vue.delete method:

Syntax:

1vue.delete( target, key )

Here the target and key are used for:

Target: It could be an object or an array.
Key: It could be a string or a number.

Example – 

HTML:

1

2

3

4

5

6

7

8

9

10

11

<center> <h3>

.delete() example

</h3> </center>

<div id="app">

   <ol>

    <li v-for="item, index in arr">

        {{item}} <br>

        <button @click="del(index)"> Delete </button>

    </li>

    </ol>

</div>

Vue.js:

1

2

3

4

5

6

7

8

9

10

11

12

new Vue({

  el: '#app',

  data: {

    arr: ['Amar', 'Akbar', 'Anthony']

  },

  methods: {

    del (index) {

    // this.arr.splice(index, 1)

    this.$delete(this.arr, index)

    }

  }

})

CSS:

1

2

3

4

5

6

7

#app {

  width: 400px;

  margin: 30px auto;

}

div {

  margin-bottom: 30px;

}

After deleting ->

VueJS – Render Function

Every Vue element utilises a render function in the Vue.js application. Mostly, the Vue compiler generates the Vue function. While we specify a template on our component, the Vue compiler will prepare the contents of this template that will deliver a rendering function. 

We have already seen the Vue components and their uses. Here, we are going to take an example to understand it more easily. Suppose if we have content that requires to be reusable across the project of Vue.js, then we can convert it as a component and use it.

Render function helps make the component dynamic and use the way it is required by keeping it common and helping pass arguments using the same component.

Example :

HTML- 

1

2

3

4

5

6

7

8

9

10

11

12

<html>

    <head>

        <title>VueJs Instance</title>

        <script type = "text/javascript" src = "js/vue.js"></script>

    </head>

    <body>

    <div id = "component_test">

<testcomponent :elementtype = "'div,blue,27,div1'">Hello Amar</testcomponent>           //component 1 for render

<testcomponent :elementtype = "'h3,green,25,h3tag'">Hello Akbar</testcomponent>         //component 2 for render

<testcomponent :elementtype = "'h1,yellow,25,h1tag'">Hello Anthony</testcomponent> </div>                 //component 3 for render

</body>

</html>

Vue.js- 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

Vue.component('testcomponent', {

    render: function(createElement) {

    var a = this.elementtype.split(",");

    return createElement(a[0], {

        attrs: {

        id: a[3],

        style: "color:" + a[1] + ";font-size:" + a[2] + ";"

        }

    },

    this.$slots.default

    )

   },

   props: {

    elementtype: {

    attributes: String,

    required: true

    }

   }

 });

var vm = new Vue({

   el: '#component_test'

 });

In the above code, we have changed the different HTML components and added a specific render function with props property using the given piece of code. Thus we can implement a Vue render function in any Vue component. Also, given Vue reactivity, the render function will be called again whenever a reactive property of the component gets updated.

VueJS – Transition & Animation

Transitions and Animations are a great way to make a website feel more contemporary and to give site visitors a better user experience. Luckily, for developers, setting up VueJS animations is quite easy. VueJS offers several ways to implicate transition to the HTML elements when they are added/updated in the DOM.

Given below is the basic syntax of a transition:

1

2

3

<transition name = "nameoftransition">

    <div></div>

</transition>

Let us consider an example:

HTML-

1

2

3

4

5

6

7

8

9

10

11

12

13

14

<center><h3>

Transition example

</h3> </center>

<div id="demo">

  <button v-on:click="show = !show">

    Click me!

  </button>

  <p>

   When we click the button the following fades :

   </p>

  <transition name="fade">

  <center> <p v-if="show">I am fading!</p></center>

  </transition>

</div>

Vue.js –

1

2

3

4

5

6

new Vue({

  el: '#demo',

  data: {

    show: true

  }

})

CSS-

1

2

3

4

5

6

.fade-enter-active, .fade-leave-active {

  transition: opacity .5s;

}

.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {

  opacity: 0;

}

Given there is a button called clickme created using which we can modify the value of the variable shown from true, to false and vice versa. There is a p tag, which showcases the text element only if the variable is true. We have enclosed the p tag with the transition element. Below are some conventional classes for transition−

  • v-enter − This class is declared initially before the element is updated. It’s the opening state.
  • v-enter-active − This class is employed to limit the delay, time span, and easing curve for starting in the transition phase. This is the active state and the class is accessible during the entire entering phase.
  • v-leave − Added when the leaving transition is triggered, discarded.
  • v-leave-active − Used during the leaving phase. It is removed when the transition is performed.

VueJS – Animation

Animations are used the same way as transition is used. Animation also possesses classes that require it to be declared for the effect to take place. Let us consider an example to observe how animation works.

Example –

HTML: 

1

2

3

4

5

6

7

8

9

10

<h3>

Example of Vue.js animations

</h3>

<div id="example">

  <center><button @click="show = !show">Animate!</button></center>

  <transition name="bounce">

    <p v-if="show">Animations work in the same way as transitions, the only contrast being that v-enter is not removed immediately after the element is inserted, but on an animationend event.</p>

  </transition>

</div>

Vue.js:

1

2

3

4

5

6

   new Vue({

  el: '#example',

  data: {

    show: true

  }

})

CSS:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

.bounce-enter-active {

  animation: bounce-in .5s;

}

.bounce-leave-active {

  animation: bounce-in .5s reverse;

}

@keyframes bounce-in {

  0% {

    transform: scale(0);

  }

  50% {

    transform: scale(1.5);

  }

  100% {

    transform: scale(1);

  }

}

To implement animation, it follows the mechanism same as transition. In the preceding code, we have an image enclosed in p tag as shown in the following piece of code.

1

2

3

<transition name = "shiftx">

   <p v-show = "show"><img src = "images/img.jpg" style = "width:100px;height:100px;" /></p>

</transition>

VueJS provides a list of custom classes, which can be added as attributes to the transition element.

  • Enter-class
  • enter-active-class
  • Leave-class
  • leave-active-class

We can implement transition and animation on the element using VueJS. Vue waits for the transition-end and animation-end event to detect if the animation or transition is done. Sometimes the transition can cause a delay. In such cases, we can apply the duration explicitly as follows.

1

2

<transition :duration = "1000"></transition>

<transition :duration = "{ enter: 500, leave: 800 }">...</transition>

You can utilise the duration property with a ‘:’ on the transition component as shown. In case there is a need to stipulate the duration distinctly for entering and exiting, it can be done as shown in the above piece of code.

VueJs – Directives

A directive is an exceptional token in the markup that tells the library to do something to a DOM element. In Vue.js, the notion of a directive is considerably simpler than that in Angular. Directives are instructions for VueJS to do things in a particular manner. We have already observed directives such as v-if, v-show, v-else, v-for, v-bind, v-model, v-on, etc. A Vue.js directive can only give the idea in the form of a prefixed HTML attribute that takes the following format:

1

2

<element prefix-directiveId="[argument:] expression [| filters...]">

</element>

Let us consider a simple example:

1. <div v-text=”message”></div>

Here the prefix is v, which is the default. The directive ID is text and the expression is a message. This directive initiates Vue.js to update the div’s textContent whenever the message property on the Vue instance changes.

Now, let us examine a more elaborate example:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

<html>

   <head>

    <title>VueJs Instance</title>

    <script type = "text/javascript" src = "js/vue.js"></script>

   </head>

   <body>

    <div id = "databinding">

        <div v-changestyle>VueJS Directive</div>

    </div>

    <script type = "text/javascript">

        Vue.directive("changestyle",{

            bind(e1,binding, vnode) {

            console.log(e1);

            e1.style.color = "blue";

            e1.style.fontSize = "35px";

            }

        });

        var vm = new Vue({

            el: '#databinding',

            data: {

            },

            methods : {

            },

        });

    </script>

   </body>

</html>

In this example, we have formulated a custom directive changestyle. We have implicated the bind method, which is a part of the directive. It takes three arguments e1, the component element to which the custom directive needs to be applied. Binding is somewhat like arguments passed to the custom directive, e.g. v-changestyle = ”{color:blue}”, where blue will be read in the binding argument and vnode is the element, i.e. nodename.

FILTERS:

VueJS provisions filters that aid with text formatting. It is employed along with v-bind and interpolations ({{}}). We want a pipe symbol at the end of JavaScript expression for filters.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

<html>

   <head>

    <title>VueJs Instance</title>

    <script type = "text/javascript" src = "js/vue.js"></script>

   </head>

   <body>

    <div id = "databinding">

        <input  v-model = "name" placeholder = "Enter Name" /><br/>

        <span style = "font-size:25px;"><b>Number of letters are : {{name |

countletters}}</b></span>

    </div>

    <script type = "text/javascript">

        var vm = new Vue({

            el: '#databinding',

            data: {

            name : ""

            },

            filters : {

            countletters : function(value) {

                return value.length;

            }

            }

        });

    </script>

   </body>

</html>

In the above example, we have made a simple filter countletters. Countletters filter calculates the numbers of characters passed into the textbox. To make use of filters, we need to use the filter property and define the filter used.

VueJS – Routing

Routing is one of the several features provided by Vue.js to permit the users to toggle between pages, eliminating the need to refresh every time a page is loaded. This results in smooth transitions between pages, giving a better feel for the user. VueJS doesn’t possess a built-in router feature. We need to entail some additional steps to install it.

Direct Download from CDN

The newest version of vue-router is available at https://unpkg.com/vue-router/dist/vue-router.js . Unpkg.com provides npm-based cdn links. The above link is always updated to the recent version. You can simply download and host it, and use it with a script tag along with vue.js.

Using NPM

Run the following command to install the vue-router.

1npm  install vue-router

Using GitHub

We can clone the repository from GitHub as follows:

1

2

3

4

git clone https://github.com/vuejs/vue-router.git node_modules/vue-router

cd node_modules/vue-router

npm install

npm run build

Let us see a simple example, which shows routing:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

<html>

   <head>

    <title>VueJs Instance</title>

    <script type = "text/javascript" src = "js/vue.js"></script>

    <script type = "text/javascript" src = "js/vue-router.js"></script>

   </head>

   <body>

    <div id = "app">

        <h1>Routing Example</h1>

        <p>

            <router-link to = "/route1">Router Link 1</router-link>

            <router-link to = "/route2">Router Link 2</router-link>

        </p>

        <!-- route outlet →

        <!-- component matched by the route will render here →

        <router-view></router-view>

    </div>

    <script type = "text/javascript">

        const Route1 = { template: '<div style =

"border-radius:20px;background-color:cyan;width:200px;height:50px;margin:10px;font-size:25px;padding:10px;">This is router 1</div>' }

    const Route2 = { template: '<div style = "border-radius:20px;background-color:green;width:200px;height:50px;margin:10px;font-size:25px;padding:10px;">This is router 2</div>' }

        const routes = [

            { path: '/route1', component: Route1 },

            { path: '/route2', component: Route2 }

        ];

        const router = new VueRouter({

            routes // short for `routes: routes`

        });

        var vm = new Vue({

            el: '#app',

            Router

        });

    </script>

   </body>

</html>

Output:

VueJS – Application example

HTML code for Furniture Inventory 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

<html>

   <head>

    <title>VueJs Instance</title>

    <script type = "text/javascript" src = "js/vue.js"></script>

   </head>

   <body>

    <style>

        #databinding{

            padding: 20px 15px 15px 15px;

            margin: 0 0 25px 0;

            width: auto;

        }

        span, option, input {

            font-size:20px;

        }

        .Table{

            display: table;

            width:80%;

        }

        .Title{

            display: table-caption;

            text-align: center;

            font-weight: bold;

            font-size: larger;

        }

        .Heading{

            display: table-row;

            font-weight: bold;

            text-align: center;

        }

        .Row{

            display: table-row;

        }

        .Cell{

            display: table-cell;

            border: solid;

            border-width: thin;

            padding-left: 5px;

            padding-right: 5px;

            width:30%;

        }

    </style>

      

    <div id = "databinding" style = "">

        <h1>Furniture Inventory</h1>

        <span>Item name</span>

        <input type = "text" placeholder = "Enter Item Name" v-model = "item"/><br>

        <span>Warehouse No.</span>

        <input type = "text" placeholder = "Enter Warehouse No." v-model = "warehouse"/><br>

        <span>Quantity</span>

        <input type = "text" placeholder = "Enter Quantity" v-model = "quantity"/><br>

        <button v-on:click = "showdata" v-bind:style = "styleobj">Add</button>

        <br/>

        <br/>

        <inventorycomponent

            v-for = "(i, index) in invdet"

            v-bind:item = "i"

            v-bind:index = "index"

            v-bind:itr = "i"

            v-bind:key = "i.item"

            v-on:removeelement = "invdet.splice(index, 1)">

        </inventorycomponent>

    </div>

      

    <script type = "text/javascript">

      

    </script>

   </body>

</html>

Vue.js code –

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

Vue.component('inventorycomponent',{

            template : '<div class = "Table"><div class = "Row"  v-bind:style = "styleobj"><div class = "Cell"><p>{{itr.item}}</p></div><div class = "Cell"><p>{{itr.warehouse}}</p></div><div class = "Cell"><p>{{itr.quantity}}</p></div><div class = "Cell"><p><button v-on:click = "$emit(\'removeelement\')">X</button></p></div></div></div>',

            props: ['itr', 'index'],

            data: function() {

            return {

                styleobj : {

                    backgroundColor:this.getcolor(),

                    fontSize : 20

                }

            }

            },

            methods:{

            getcolor : function() {

                if (this.index % 2) {

                    return "#66CDAA";

                } else {

                    return "#90EE90";

                }

            }

            }

        });

        var vm = new Vue({

            el: '#databinding',

            data: {

            item:'',

            warehouse:'',

            quantity : '',

            invdet:[],

            styleobj: {

                backgroundColor: '#2196F3!important',

                cursor: 'pointer',

                padding: '8px 16px',

                verticalAlign: 'middle',

            }

            },

            methods :{

            showdata : function() {

                this.invdet.push({

                    item: this.item,

                    warehouse: this.warehouse,

                    quantity : this.quantity

                });

                this.item = "";

                this.warehouse = "";

                this.quantity = "";

            }

            }

        });

In this example, we have three textboxes and via the binding properties for components, we can now showcase the details entered in them through a table rendered by a list. Following are the actions you can perform –

  • Add item

  • Delete item

We have used the emit event for deletion of a table row. 

  • After deleting –

This brings us to the end of the blog on VueJS Tutorial. We hope that you enjoyed this VueJS Tutorial and were able to gain knowledge from the same. If you wish to learn more such concepts, you can head over to Great Learning Academy and check out the free online courses available.


Original article source at: https://www.mygreatlearning.com

#vuejs 

What Is VueJS? | A VueJS Tutorial
Monty  Boehm

Monty Boehm

1669029120

How to Developing A Single Page App with Flask and Vue.js

The following is a step-by-step walkthrough of how to set up a basic CRUD app with Vue and Flask. We'll start by scaffolding a new Vue application with the Vue CLI and then move on to performing the basic CRUD operations through a back-end RESTful API powered by Python and Flask.

Final app:

final app

Main dependencies:

  • Vue v2.6.11
  • Vue CLI v4.5.11
  • Node v15.7.0
  • npm v7.4.3
  • Flask v1.1.2
  • Python v3.9.1

Objectives

By the end of this tutorial, you will be able to:

  1. Explain what Flask is
  2. Explain what Vue is and how it compares to other UI libraries and front-end frameworks like React and Angular
  3. Scaffold a Vue project using the Vue CLI
  4. Create and render Vue components in the browser
  5. Create a Single Page Application (SPA) with Vue components
  6. Connect a Vue application to a Flask back-end
  7. Develop a RESTful API with Flask
  8. Style Vue Components with Bootstrap
  9. Use the Vue Router to create routes and render components

Flask and Vue

Let's quickly look at each framework.

What is Flask?

Flask is a simple, yet powerful micro web framework for Python, perfect for building RESTful APIs. Like Sinatra (Ruby) and Express (Node), it's minimal and flexible, so you can start small and build up to a more complex app as needed.

First time with Flask? Check out the following two resources:

  1. Flaskr TDD
  2. Developing Web Applications with Python and Flask

What is Vue?

Vue is an open-source JavaScript framework used for building user interfaces. It adopted some of the best practices from React and Angular. That said, compared to React and Angular, it's much more approachable, so beginners can get up and running quickly. It's also just as powerful, so it provides all the features you'll need to create modern front-end applications.

For more on Vue, along with the pros and cons of using it vs. React and Angular, review the resources:

  1. Vue: Comparison with Other Frameworks
  2. Learn Vue by Building and Deploying a CRUD App
  3. React vs Angular vs Vue.js

First time with Vue? Take a moment to read through the Introduction from the official Vue guide.

Flask Setup

Begin by creating a new project directory:

$ mkdir flask-vue-crud
$ cd flask-vue-crud

Within "flask-vue-crud", create a new directory called "server". Then, create and activate a virtual environment inside the "server" directory:

$ python3.9 -m venv env
$ source env/bin/activate
(env)$

The above commands may differ depending on your environment.

Install Flask along with the Flask-CORS extension:

(env)$ pip install Flask==1.1.2 Flask-Cors==3.0.10

Add an app.py file to the newly created "server" directory:

from flask import Flask, jsonify
from flask_cors import CORS


# configuration
DEBUG = True

# instantiate the app
app = Flask(__name__)
app.config.from_object(__name__)

# enable CORS
CORS(app, resources={r'/*': {'origins': '*'}})


# sanity check route
@app.route('/ping', methods=['GET'])
def ping_pong():
    return jsonify('pong!')


if __name__ == '__main__':
    app.run()

Why do we need Flask-CORS? In order to make cross-origin requests -- e.g., requests that originate from a different protocol, IP address, domain name, or port -- you need to enable Cross Origin Resource Sharing (CORS). Flask-CORS handles this for us.

It's worth noting that the above setup allows cross-origin requests on all routes, from any domain, protocol, or port. In a production environment, you should only allow cross-origin requests from the domain where the front-end application is hosted. Refer to the Flask-CORS documentation for more info on this.

Run the app:

(env)$ python app.py

To test, point your browser at http://localhost:5000/ping. You should see:

"pong!"

Back in the terminal, press Ctrl+C to kill the server and then navigate back to the project root. With that, let's turn our attention to the front-end and get Vue set up.

Vue Setup

We'll be using the powerful Vue CLI to generate a customized project boilerplate.

Install it globally:

$ npm install -g @vue/cli@4.5.11

First time with npm? Review the official About npm guide.

Then, within "flask-vue-crud", run the following command to initialize a new Vue project called client:

$ vue create client

This will require you to answer a few questions about the project.

Vue CLI v4.5.11
? Please pick a preset: (Use arrow keys)
❯ Default ([Vue 2] babel, eslint)
  Default (Vue 3 Preview) ([Vue 3] babel, eslint)
  Manually select features

Use the down arrow key to highlight "Manually select features", and then press enter. Next, you'll need to select the features you'd like to install. For this tutorial, select "Choose Vue version", "Babel", "Router", and "Linter / Formatter" like so:

Vue CLI v4.5.11
? Please pick a preset: Manually select features
? Check the features needed for your project:
❯◉ Choose Vue version
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◉ Router
 ◯ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing

Press enter.

Select "2.x" for the Vue version. Use the history mode for the router. Select "ESLint + Airbnb config" for the linter and "Lint on save". Finally, select the "In package.json" option so that configuration is placed in the package.json file instead of in separate configuration files.

You should see something similar to:

Vue CLI v4.5.11
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Router, Linter
? Choose a version of Vue.js that you want to start the project with 2.x
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: Airbnb
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In package.json
? Save this as a preset for future projects? (y/N) No

Press enter again to configure the project structure and install the dependencies.

Take a quick look at the generated project structure. It may seem like a lot, but we'll only be dealing with the files and folders in the "src" folder along with the index.html file found in the "public" folder.

The index.html file is the starting point of our Vue application.

<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

Take note of the <div> element with an id of app. This is a placeholder that Vue will use to attach the generated HTML and CSS to produce the UI.

Turn your attention to the folders inside the "src" folder:

client/src
├── App.vue
├── assets
│   └── logo.png
├── components
│   └── HelloWorld.vue
├── main.js
├── router
│   └── index.js
└── views
    ├── About.vue
    └── Home.vue

Breakdown:

NamePurpose
main.jsapp entry point, which loads and initializes Vue along with the root component
App.vueRoot component, which is the starting point from which all other components will be rendered
"components"where UI components are stored
router/index.jswhere URLS are defined and mapped to components
"views"where UI components that are tied to the router are stored
"assets"where static assets, like images and fonts, are stored

Review the client/src/components/HelloWorld.vue file. This is a Single File component, which is broken up into three different sections:

  1. template: for component-specific HTML
  2. script: where the component logic is implemented via JavaScript
  3. style: for CSS styles

Fire up the development server:

$ cd client
$ npm run serve

Navigate to http://localhost:8080 in the browser of your choice. You should see the following:

default vue app

To simplify things, remove the "client/src/views" folder. Then, add a new component to the "client/src/components" folder called Ping.vue:

<template>
  <div>
    <p>{{ msg }}</p>
  </div>
</template>

<script>
export default {
  name: 'Ping',
  data() {
    return {
      msg: 'Hello!',
    };
  },
};
</script>

Update client/src/router/index.js to map '/ping' to the Ping component like so:

import Vue from 'vue';
import Router from 'vue-router';
import Ping from '../components/Ping.vue';

Vue.use(Router);

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/ping',
      name: 'Ping',
      component: Ping,
    },
  ],
});

Finally, within client/src/App.vue, remove the navigation along with the styles:

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

You should now see Hello! in the browser at http://localhost:8080/ping.

To connect the client-side Vue app with the back-end Flask app, we can use the axios library to send AJAX requests.

Start by installing it:

$ npm install axios@0.21.1 --save

Update the script section of the component, in Ping.vue, like so:

<script>
import axios from 'axios';

export default {
  name: 'Ping',
  data() {
    return {
      msg: '',
    };
  },
  methods: {
    getMessage() {
      const path = 'http://localhost:5000/ping';
      axios.get(path)
        .then((res) => {
          this.msg = res.data;
        })
        .catch((error) => {
          // eslint-disable-next-line
          console.error(error);
        });
    },
  },
  created() {
    this.getMessage();
  },
};
</script>

Fire up the Flask app in a new terminal window. You should see pong! in the browser. Essentially, when a response is returned from the back-end, we set msg to the value of data from the response object.

Bootstrap Setup

Next, let's add Bootstrap, a popular CSS framework, to the app so we can quickly add some style.

Install:

$ npm install bootstrap@4.6.0 --save

Ignore the warnings for jquery and popper.js. Do NOT add either to your project. More on this later.

Import the Bootstrap styles to client/src/main.js:

import Vue from 'vue';
import App from './App.vue';
import router from './router';
import 'bootstrap/dist/css/bootstrap.css';

Vue.config.productionTip = false;

new Vue({
  router,
  render: (h) => h(App),
}).$mount('#app');

Update the style section in client/src/App.vue:

<style>
#app {
  margin-top: 60px
}
</style>

Ensure Bootstrap is wired up correctly by using a Button and Container in the Ping component:

<template>
  <div class="container">
    <button type="button" class="btn btn-primary">{{ msg }}</button>
  </div>
</template>

Run the dev server:

$ npm run serve

You should see:

vue with bootstrap

Next, add a new component called Books in a new file called Books.vue:

<template>
  <div class="container">
    <p>books</p>
  </div>
</template>

Update the router:

import Vue from 'vue';
import Router from 'vue-router';
import Books from '../components/Books.vue';
import Ping from '../components/Ping.vue';

Vue.use(Router);

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'Books',
      component: Books,
    },
    {
      path: '/ping',
      name: 'Ping',
      component: Ping,
    },
  ],
});

Test:

  1. http://localhost:8080
  2. http://localhost:8080/ping

Finally, let's add a quick, Bootstrap-styled table to the Books component:

<template>
  <div class="container">
    <div class="row">
      <div class="col-sm-10">
        <h1>Books</h1>
        <hr><br><br>
        <button type="button" class="btn btn-success btn-sm">Add Book</button>
        <br><br>
        <table class="table table-hover">
          <thead>
            <tr>
              <th scope="col">Title</th>
              <th scope="col">Author</th>
              <th scope="col">Read?</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>foo</td>
              <td>bar</td>
              <td>foobar</td>
              <td>
                <div class="btn-group" role="group">
                  <button type="button" class="btn btn-warning btn-sm">Update</button>
                  <button type="button" class="btn btn-danger btn-sm">Delete</button>
                </div>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</template>

You should now see:

books component

Now we can start building out the functionality of our CRUD app.

What are we Building?

Our goal is to design a back-end RESTful API, powered by Python and Flask, for a single resource -- books. The API itself should follow RESTful design principles, using the basic HTTP verbs: GET, POST, PUT, and DELETE.

We'll also set up a front-end application with Vue that consumes the back-end API:

final app

This tutorial only deals with the happy path. Handling errors is a separate exercise. Check your understanding and add proper error handling on both the front and back-end.

GET Route

Server

Add a list of books to server/app.py:

BOOKS = [
    {
        'title': 'On the Road',
        'author': 'Jack Kerouac',
        'read': True
    },
    {
        'title': 'Harry Potter and the Philosopher\'s Stone',
        'author': 'J. K. Rowling',
        'read': False
    },
    {
        'title': 'Green Eggs and Ham',
        'author': 'Dr. Seuss',
        'read': True
    }
]

Add the route handler:

@app.route('/books', methods=['GET'])
def all_books():
    return jsonify({
        'status': 'success',
        'books': BOOKS
    })

Run the Flask app, if it's not already running, and then manually test out the route at http://localhost:5000/books.

Looking for an extra challenge? Write an automated test for this. Review this resource for more info on testing a Flask app.

Client

Update the component:

<template>
  <div class="container">
    <div class="row">
      <div class="col-sm-10">
        <h1>Books</h1>
        <hr><br><br>
        <button type="button" class="btn btn-success btn-sm">Add Book</button>
        <br><br>
        <table class="table table-hover">
          <thead>
            <tr>
              <th scope="col">Title</th>
              <th scope="col">Author</th>
              <th scope="col">Read?</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="(book, index) in books" :key="index">
              <td>{{ book.title }}</td>
              <td>{{ book.author }}</td>
              <td>
                <span v-if="book.read">Yes</span>
                <span v-else>No</span>
              </td>
              <td>
                <div class="btn-group" role="group">
                  <button type="button" class="btn btn-warning btn-sm">Update</button>
                  <button type="button" class="btn btn-danger btn-sm">Delete</button>
                </div>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      books: [],
    };
  },
  methods: {
    getBooks() {
      const path = 'http://localhost:5000/books';
      axios.get(path)
        .then((res) => {
          this.books = res.data.books;
        })
        .catch((error) => {
          // eslint-disable-next-line
          console.error(error);
        });
    },
  },
  created() {
    this.getBooks();
  },
};
</script>

After the component is initialized, the getBooks() method is called via the created lifecycle hook, which fetches the books from the back-end endpoint we just set up.

Review Instance Lifecycle Hooks for more on the component lifecycle and the available methods.

In the template, we iterated through the list of books via the v-for directive, creating a new table row on each iteration. The index value is used as the key. Finally, v-if is then used to render either Yes or No, indicating whether the user has read the book or not.

books component

Bootstrap Vue

In the next section, we'll use a modal to add a new book. We'll add on the Bootstrap Vue library for this, which provides a set of Vue components styled with Bootstrap-based HTML and CSS.

Why Bootstrap Vue? Bootstrap's Modal component uses jQuery, which you should avoid using with Vue together in the same project since Vue uses the Virtual Dom to update the DOM. In other words, if you did use jQuery to manipulate the DOM, Vue would not know about it. At the very least, if you absolutely need to use jQuery, do not use Vue and jQuery together on the same DOM elements.

Install:

$ npm install bootstrap-vue@2.21.2 --save

Enable the Bootstrap Vue library in client/src/main.js:

import BootstrapVue from 'bootstrap-vue';
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import 'bootstrap/dist/css/bootstrap.css';

Vue.use(BootstrapVue);

Vue.config.productionTip = false;

new Vue({
  router,
  render: (h) => h(App),
}).$mount('#app');

POST Route

Server

Update the existing route handler to handle POST requests for adding a new book:

@app.route('/books', methods=['GET', 'POST'])
def all_books():
    response_object = {'status': 'success'}
    if request.method == 'POST':
        post_data = request.get_json()
        BOOKS.append({
            'title': post_data.get('title'),
            'author': post_data.get('author'),
            'read': post_data.get('read')
        })
        response_object['message'] = 'Book added!'
    else:
        response_object['books'] = BOOKS
    return jsonify(response_object)

Update the imports:

from flask import Flask, jsonify, request

With the Flask server running, you can test the POST route in a new terminal tab:

$ curl -X POST http://localhost:5000/books -d \
  '{"title": "1Q84", "author": "Haruki Murakami", "read": "true"}' \
  -H 'Content-Type: application/json'

You should see:

{
  "message": "Book added!",
  "status": "success"
}

You should also see the new book in the response from the http://localhost:5000/books endpoint.

What if the title already exists? Or what if a title has more than one author? Check your understanding by handling these cases. Also, how would you handle an invalid payload where the title, author, and/or read is missing?

Client

On the client-side, let's add that modal now for adding a new book to the Books component, starting with the HTML:

<b-modal ref="addBookModal"
         id="book-modal"
         title="Add a new book"
         hide-footer>
  <b-form @submit="onSubmit" @reset="onReset" class="w-100">
  <b-form-group id="form-title-group"
                label="Title:"
                label-for="form-title-input">
      <b-form-input id="form-title-input"
                    type="text"
                    v-model="addBookForm.title"
                    required
                    placeholder="Enter title">
      </b-form-input>
    </b-form-group>
    <b-form-group id="form-author-group"
                  label="Author:"
                  label-for="form-author-input">
        <b-form-input id="form-author-input"
                      type="text"
                      v-model="addBookForm.author"
                      required
                      placeholder="Enter author">
        </b-form-input>
      </b-form-group>
    <b-form-group id="form-read-group">
      <b-form-checkbox-group v-model="addBookForm.read" id="form-checks">
        <b-form-checkbox value="true">Read?</b-form-checkbox>
      </b-form-checkbox-group>
    </b-form-group>
    <b-button type="submit" variant="primary">Submit</b-button>
    <b-button type="reset" variant="danger">Reset</b-button>
  </b-form>
</b-modal>

Add this just before the closing div tag. Take a quick look at the code. v-model is a directive used to bind input values back to the state. You'll see this in action shortly.

What does hide-footer do? Review this on your own in the Bootstrap Vue docs.

Update the script section:

<script>
import axios from 'axios';

export default {
  data() {
    return {
      books: [],
      addBookForm: {
        title: '',
        author: '',
        read: [],
      },
    };
  },
  methods: {
    getBooks() {
      const path = 'http://localhost:5000/books';
      axios.get(path)
        .then((res) => {
          this.books = res.data.books;
        })
        .catch((error) => {
          // eslint-disable-next-line
          console.error(error);
        });
    },
    addBook(payload) {
      const path = 'http://localhost:5000/books';
      axios.post(path, payload)
        .then(() => {
          this.getBooks();
        })
        .catch((error) => {
          // eslint-disable-next-line
          console.log(error);
          this.getBooks();
        });
    },
    initForm() {
      this.addBookForm.title = '';
      this.addBookForm.author = '';
      this.addBookForm.read = [];
    },
    onSubmit(evt) {
      evt.preventDefault();
      this.$refs.addBookModal.hide();
      let read = false;
      if (this.addBookForm.read[0]) read = true;
      const payload = {
        title: this.addBookForm.title,
        author: this.addBookForm.author,
        read, // property shorthand
      };
      this.addBook(payload);
      this.initForm();
    },
    onReset(evt) {
      evt.preventDefault();
      this.$refs.addBookModal.hide();
      this.initForm();
    },
  },
  created() {
    this.getBooks();
  },
};
</script>

What's happening here?

  1. addBookForm is bound to the form inputs via, again, v-model. When one is updated, the other will be updated as well, in other words. This is called two-way binding. Take a moment to read about it here. Think about the ramifications of this. Do you think this makes state management easier or harder? How do React and Angular handle this? In my opinion, two-way binding (along with mutability) makes Vue much more approachable than React.
  2. onSubmit is fired when the user submits the form successfully. On submit, we prevent the normal browser behavior (evt.preventDefault()), close the modal (this.$refs.addBookModal.hide()), fire the addBook method, and clear the form (initForm()).
  3. addBook sends a POST request to /books to add a new book.

Review the rest of the changes on your own, referencing the Vue docs as necessary.

Can you think of any potential errors on the client or server? Handle these on your own to improve user experience.

Finally, update the "Add Book" button in the template so that the modal is displayed when the button is clicked:

<button type="button" class="btn btn-success btn-sm" v-b-modal.book-modal>Add Book</button>

The component should now look like this:

<template>
  <div class="container">
    <div class="row">
      <div class="col-sm-10">
        <h1>Books</h1>
        <hr><br><br>
        <button type="button" class="btn btn-success btn-sm" v-b-modal.book-modal>Add Book</button>
        <br><br>
        <table class="table table-hover">
          <thead>
            <tr>
              <th scope="col">Title</th>
              <th scope="col">Author</th>
              <th scope="col">Read?</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="(book, index) in books" :key="index">
              <td>{{ book.title }}</td>
              <td>{{ book.author }}</td>
              <td>
                <span v-if="book.read">Yes</span>
                <span v-else>No</span>
              </td>
              <td>
                <div class="btn-group" role="group">
                  <button type="button" class="btn btn-warning btn-sm">Update</button>
                  <button type="button" class="btn btn-danger btn-sm">Delete</button>
                </div>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
    <b-modal ref="addBookModal"
            id="book-modal"
            title="Add a new book"
            hide-footer>
      <b-form @submit="onSubmit" @reset="onReset" class="w-100">
      <b-form-group id="form-title-group"
                    label="Title:"
                    label-for="form-title-input">
          <b-form-input id="form-title-input"
                        type="text"
                        v-model="addBookForm.title"
                        required
                        placeholder="Enter title">
          </b-form-input>
        </b-form-group>
        <b-form-group id="form-author-group"
                      label="Author:"
                      label-for="form-author-input">
            <b-form-input id="form-author-input"
                          type="text"
                          v-model="addBookForm.author"
                          required
                          placeholder="Enter author">
            </b-form-input>
          </b-form-group>
        <b-form-group id="form-read-group">
          <b-form-checkbox-group v-model="addBookForm.read" id="form-checks">
            <b-form-checkbox value="true">Read?</b-form-checkbox>
          </b-form-checkbox-group>
        </b-form-group>
        <b-button-group>
          <b-button type="submit" variant="primary">Submit</b-button>
          <b-button type="reset" variant="danger">Reset</b-button>
        </b-button-group>
      </b-form>
    </b-modal>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      books: [],
      addBookForm: {
        title: '',
        author: '',
        read: [],
      },
    };
  },
  methods: {
    getBooks() {
      const path = 'http://localhost:5000/books';
      axios.get(path)
        .then((res) => {
          this.books = res.data.books;
        })
        .catch((error) => {
          // eslint-disable-next-line
          console.error(error);
        });
    },
    addBook(payload) {
      const path = 'http://localhost:5000/books';
      axios.post(path, payload)
        .then(() => {
          this.getBooks();
        })
        .catch((error) => {
          // eslint-disable-next-line
          console.log(error);
          this.getBooks();
        });
    },
    initForm() {
      this.addBookForm.title = '';
      this.addBookForm.author = '';
      this.addBookForm.read = [];
    },
    onSubmit(evt) {
      evt.preventDefault();
      this.$refs.addBookModal.hide();
      let read = false;
      if (this.addBookForm.read[0]) read = true;
      const payload = {
        title: this.addBookForm.title,
        author: this.addBookForm.author,
        read, // property shorthand
      };
      this.addBook(payload);
      this.initForm();
    },
    onReset(evt) {
      evt.preventDefault();
      this.$refs.addBookModal.hide();
      this.initForm();
    },
  },
  created() {
    this.getBooks();
  },
};
</script>

Test it out! Try adding a book:

add new book

Alert Component

Next, let's add an Alert component to display a message to the end user after a new book is added. We'll create a new component for this since it's likely that you'll use the functionality in a number of components.

Add a new file called Alert.vue to "client/src/components":

<template>
  <p>It works!</p>
</template>

Then, import it into the script section of the Books component and register the component:

<script>
import axios from 'axios';
import Alert from './Alert.vue';

...

export default {
  data() {
    return {
      books: [],
      addBookForm: {
        title: '',
        author: '',
        read: [],
      },
    };
  },
  components: {
    alert: Alert,
  },

  ...

};
</script>

Now, we can reference the new component in the template section:

<template>
  <b-container>
    <b-row>
      <b-col col sm="10">
        <h1>Books</h1>
        <hr><br><br>
        <alert></alert>
        <button type="button" class="btn btn-success btn-sm" v-b-modal.book-modal>Add Book</button>

        ...

      </b-col>
    </b-row>
  </b-container>
</template>

Refresh the browser. You should now see:

bootstrap alert

Review Composing with Components from the official Vue docs for more info on working with components in other components.

Next, let's add the actual b-alert component client/src/components/Alert.vue:

<template>
  <div>
    <b-alert variant="success" show>{{ message }}</b-alert>
    <br>
  </div>
</template>

<script>
export default {
  props: ['message'],
};
</script>

Take note of the props option in the script section. We can pass a message down from the parent component (Books) like so:

<alert message="hi"></alert>

Try this out:

bootstrap alert

Review the docs for more info on props.

To make it dynamic, so that a custom message is passed down, use a binding expression in Books.vue:

<alert :message="message"></alert>

Add the message to the data options, in Books.vue as well:

data() {
  return {
    books: [],
    addBookForm: {
      title: '',
      author: '',
      read: [],
    },
    message: '',
  };
},

Then, within addBook, update the message:

addBook(payload) {
  const path = 'http://localhost:5000/books';
  axios.post(path, payload)
    .then(() => {
      this.getBooks();
      this.message = 'Book added!';
    })
    .catch((error) => {
      // eslint-disable-next-line
      console.log(error);
      this.getBooks();
    });
},

Finally, add a v-if, so the alert is only displayed if showMessage is true:

<alert :message=message v-if="showMessage"></alert>

Add showMessage to the data:

data() {
  return {
    books: [],
    addBookForm: {
      title: '',
      author: '',
      read: [],
    },
    message: '',
    showMessage: false,
  };
},

Update addBook again, setting showMessage to true:

addBook(payload) {
  const path = 'http://localhost:5000/books';
  axios.post(path, payload)
    .then(() => {
      this.getBooks();
      this.message = 'Book added!';
      this.showMessage = true;
    })
    .catch((error) => {
      // eslint-disable-next-line
      console.log(error);
      this.getBooks();
    });
},

Test it out!

add new book

Challenges:

  1. Think about where showMessage should be set to false. Update your code.
  2. Try using the Alert component to display errors.
  3. Refactor the alert to be dismissible.

PUT Route

Server

For updates, we'll need to use a unique identifier since we can't depend on the title to be unique. We can use uuid from the Python standard library.

Update BOOKS in server/app.py:

BOOKS = [
    {
        'id': uuid.uuid4().hex,
        'title': 'On the Road',
        'author': 'Jack Kerouac',
        'read': True
    },
    {
        'id': uuid.uuid4().hex,
        'title': 'Harry Potter and the Philosopher\'s Stone',
        'author': 'J. K. Rowling',
        'read': False
    },
    {
        'id': uuid.uuid4().hex,
        'title': 'Green Eggs and Ham',
        'author': 'Dr. Seuss',
        'read': True
    }
]

Don't forget the import:

import uuid

Refactor all_books to account for the unique id when a new book is added:

@app.route('/books', methods=['GET', 'POST'])
def all_books():
    response_object = {'status': 'success'}
    if request.method == 'POST':
        post_data = request.get_json()
        BOOKS.append({
            'id': uuid.uuid4().hex,
            'title': post_data.get('title'),
            'author': post_data.get('author'),
            'read': post_data.get('read')
        })
        response_object['message'] = 'Book added!'
    else:
        response_object['books'] = BOOKS
    return jsonify(response_object)

Add a new route handler:

@app.route('/books/<book_id>', methods=['PUT'])
def single_book(book_id):
    response_object = {'status': 'success'}
    if request.method == 'PUT':
        post_data = request.get_json()
        remove_book(book_id)
        BOOKS.append({
            'id': uuid.uuid4().hex,
            'title': post_data.get('title'),
            'author': post_data.get('author'),
            'read': post_data.get('read')
        })
        response_object['message'] = 'Book updated!'
    return jsonify(response_object)

Add the helper:

def remove_book(book_id):
    for book in BOOKS:
        if book['id'] == book_id:
            BOOKS.remove(book)
            return True
    return False

Take a moment to think about how you would handle the case of an id not existing. What if the payload is not correct? Refactor the for loop in the helper as well so that it's more Pythonic.

Client

Steps:

  1. Add modal and form
  2. Handle update button click
  3. Wire up AJAX request
  4. Alert user
  5. Handle cancel button click

(1) Add modal and form

First, add a new modal to the template, just below the first modal:

<b-modal ref="editBookModal"
         id="book-update-modal"
         title="Update"
         hide-footer>
  <b-form @submit="onSubmitUpdate" @reset="onResetUpdate" class="w-100">
  <b-form-group id="form-title-edit-group"
                label="Title:"
                label-for="form-title-edit-input">
      <b-form-input id="form-title-edit-input"
                    type="text"
                    v-model="editForm.title"
                    required
                    placeholder="Enter title">
      </b-form-input>
    </b-form-group>
    <b-form-group id="form-author-edit-group"
                  label="Author:"
                  label-for="form-author-edit-input">
        <b-form-input id="form-author-edit-input"
                      type="text"
                      v-model="editForm.author"
                      required
                      placeholder="Enter author">
        </b-form-input>
      </b-form-group>
    <b-form-group id="form-read-edit-group">
      <b-form-checkbox-group v-model="editForm.read" id="form-checks">
        <b-form-checkbox value="true">Read?</b-form-checkbox>
      </b-form-checkbox-group>
    </b-form-group>
    <b-button-group>
      <b-button type="submit" variant="primary">Update</b-button>
      <b-button type="reset" variant="danger">Cancel</b-button>
    </b-button-group>
  </b-form>
</b-modal>

Add the form state to the data part of the script section:

editForm: {
  id: '',
  title: '',
  author: '',
  read: [],
},

Challenge: Instead of using a new modal, try using the same modal for handling both POST and PUT requests.

(2) Handle update button click

Update the "update" button in the table:

<button
        type="button"
        class="btn btn-warning btn-sm"
        v-b-modal.book-update-modal
        @click="editBook(book)">
    Update
</button>

Add a new method to update the values in editForm:

editBook(book) {
  this.editForm = book;
},

Then, add a method to handle the form submit:

onSubmitUpdate(evt) {
  evt.preventDefault();
  this.$refs.editBookModal.hide();
  let read = false;
  if (this.editForm.read[0]) read = true;
  const payload = {
    title: this.editForm.title,
    author: this.editForm.author,
    read,
  };
  this.updateBook(payload, this.editForm.id);
},

(3) Wire up AJAX request

updateBook(payload, bookID) {
  const path = `http://localhost:5000/books/${bookID}`;
  axios.put(path, payload)
    .then(() => {
      this.getBooks();
    })
    .catch((error) => {
      // eslint-disable-next-line
      console.error(error);
      this.getBooks();
    });
},

(4) Alert user

Update updateBook:

updateBook(payload, bookID) {
  const path = `http://localhost:5000/books/${bookID}`;
  axios.put(path, payload)
    .then(() => {
      this.getBooks();
      this.message = 'Book updated!';
      this.showMessage = true;
    })
    .catch((error) => {
      // eslint-disable-next-line
      console.error(error);
      this.getBooks();
    });
},

(5) Handle cancel button click

Add method:

onResetUpdate(evt) {
  evt.preventDefault();
  this.$refs.editBookModal.hide();
  this.initForm();
  this.getBooks(); // why?
},

Update initForm:

initForm() {
  this.addBookForm.title = '';
  this.addBookForm.author = '';
  this.addBookForm.read = [];
  this.editForm.id = '';
  this.editForm.title = '';
  this.editForm.author = '';
  this.editForm.read = [];
},

Make sure to review the code before moving on. Once done, test out the application. Ensure the modal is displayed on the button click and that the input values are populated correctly.

update book

DELETE Route

Server

Update the route handler:

@app.route('/books/<book_id>', methods=['PUT', 'DELETE'])
def single_book(book_id):
    response_object = {'status': 'success'}
    if request.method == 'PUT':
        post_data = request.get_json()
        remove_book(book_id)
        BOOKS.append({
            'id': uuid.uuid4().hex,
            'title': post_data.get('title'),
            'author': post_data.get('author'),
            'read': post_data.get('read')
        })
        response_object['message'] = 'Book updated!'
    if request.method == 'DELETE':
        remove_book(book_id)
        response_object['message'] = 'Book removed!'
    return jsonify(response_object)

Client

Update the "delete" button like so:

<button
        type="button"
        class="btn btn-danger btn-sm"
        @click="onDeleteBook(book)">
    Delete
</button>

Add the methods to handle the button click and then remove the book:

removeBook(bookID) {
  const path = `http://localhost:5000/books/${bookID}`;
  axios.delete(path)
    .then(() => {
      this.getBooks();
      this.message = 'Book removed!';
      this.showMessage = true;
    })
    .catch((error) => {
      // eslint-disable-next-line
      console.error(error);
      this.getBooks();
    });
},
onDeleteBook(book) {
  this.removeBook(book.id);
},

Now, when the user clicks the delete button, the onDeleteBook method is fired, which, in turn, fires the removeBook method. This method sends the DELETE request to the back-end. When the response comes back, the alert message is displayed and getBooks is ran.

Challenges:

  1. Instead of deleting on the button click, add a confirmation alert.
  2. Display a message saying, like "No books! Please add one.", when no books are present.

delete book

Conclusion

This post covered the basics of setting up a CRUD app with Vue and Flask.

Check your understanding by reviewing the objectives from the beginning of this post and going through each of the challenges.

You can find the source code in the flask-vue-crud repo. Thanks for reading.

Looking for more?

  1. Check out the Accepting Payments with Stripe, Vue.js, and Flask blog post, which starts from where this post leaves off.
  2. Want to learn how to deploy this app to Heroku? Check out Deploying a Flask and Vue App to Heroku with Docker and Gitlab CI.

Original article source at: https://testdriven.io/

#flask #vue #api 

How to Developing A Single Page App with Flask and Vue.js
Nigel  Uys

Nigel Uys

1669012260

Should you eject your Create React App?

My opinion on should you eject your Create React App configuration

Create React App is a program that helps you to create single-page React applications with all the standard configurations. You can think of Create React App as a builder that handles the building of your React app, allowing you to focus on writing the code.

To make the browser understand React-based code, you need to configure tools like Webpack and Babel. With Create React App, these tools are preconfigured and hidden. Just create a new application and you’re good to go.

But because Create React App is pre-configured, that means there might be some settings that you aren’t satisfied with. Maybe you want to use LESS to write your style. Maybe you want to use Webpack’s latest version with your React application (As of this writing, Webpack 5 has been released but CRA still use Webpack 4)

To handle this case, Create React App created a special command that allows you to spit out the configuration files so that you can customize it as you need:

npm run eject

When you run npm run eject command in your React application, you will be able to edit the configuration and script files. You also can upgrade or downgrade the dependencies version on the ejected package.json file.

But the eject command comes with a price. Once you eject, you can’t go back and hide the configuration files. You will have to maintain your React app configuration on your own. This means:

  • You need to update the dependencies and ensure its not broken when a new version is released
  • If Create React App released a new version, you can’t update your application to the new version

In addition, the React scripts directory which contains the Webpack process is quite complicated. You need to have a good understanding of how Webpack works to maintain it.

Now that you know the cost of ejecting, it’s time to answer the question.

Should you eject your Create React App?

Create React App was created with the community feedback. New features have been added to it throughout the years in order to fit the need of common React projects. If you’re confident that you can maintain the configuration, I suggest you write your own configuration from scratch. That way, you won’t be confused by any configuration that you didn’t write yourself.

Developers also have created a workaround so that you can tweak Webpack configurations without ejecting. React App Rewired is one such example. But even React App Rewired doesn’t have any warranty. If it breaks, you need to fix it yourself or find community help.

If you don’t want to maintain your own configuration and you aren’t satisfied with CRA configs, you may be better to use other React application builder like Next.js, which supports custom webpack config.

Conclusion

Create React App is a convenient tool that allows you to develop React application without having to configure the required build tools. But it’s an opinionated tool, which means there will always be some unsupported “way” of building React application. When you find CRA configuration lacking, you can eject the configuration so you can edit it, but you’ll need to maintain your configuration.

If you ask me, I recommend you to either write and maintain your own configuration or you use other React app builder like Next.js, which has custom webpack config built-in. If you use CRA, then accept the limitations and don’t eject from it.

Original article source at: https://sebhastian.com/

#react #app 

Should you eject your Create React App?
Bongani  Ngema

Bongani Ngema

1668861960

How to Developing A Single Page App with FastAPI and Vue.js

The following is a step-by-step walkthrough of how to build and containerize a basic CRUD app with FastAPI, Vue, Docker, and Postgres. We'll start in the backend, developing a RESTful API powered by Python, FastAPI, and Docker and then move on the frontend. We'll also wire up token-based authentication.

Final app:

final app

Main dependencies:

  • Vue v2.6.11
  • Vue CLI v4.5.13
  • Node v16.6.1
  • npm v7.20.3
  • FastAPI v0.68.0
  • Python v3.9

This is an intermediate-level tutorial, which focuses on developing backend and frontend apps with FastAPI and Vue, respectively. Along with the apps themselves, you'll add authentication and integrate them together. It's assumed that you have experience with FastAPI, Vue, and Docker. See the FastAPI and Vue section for recommended resources for learning the previously mentioned tools and technologies.

Objectives

By the end of this tutorial, you will be able to:

  1. Explain what FastAPI is
  2. Explain what Vue is and how it compares to other UI libraries and frontend frameworks like React and Angular
  3. Develop a RESTful API with FastAPI
  4. Scaffold a Vue project using the Vue CLI
  5. Create and render Vue components in the browser
  6. Create a Single Page Application (SPA) with Vue components
  7. Connect a Vue application to a FastAPI back-end
  8. Style Vue Components with Bootstrap
  9. Use the Vue Router to create routes and render components
  10. Manage user auth with token-based authentication

FastAPI and Vue

Let's quickly look at each framework.

What is FastAPI?

FastAPI is a modern, batteries-included Python web framework that's perfect for building RESTful APIs. It can handle both synchronous and asynchronous requests and has built-in support for data validation, JSON serialization, authentication and authorization, and OpenAPI documentation.

Highlights:

  1. Heavily inspired by Flask, it has a lightweight microframework feel with support for Flask-like route decorators.
  2. It takes advantage of Python type hints for parameter declaration which enables data validation (via pydantic) and OpenAPI/Swagger documentation.
  3. Built on top of Starlette, it supports the development of asynchronous APIs.
  4. It's fast. Since async is much more efficient than the traditional synchronous threading model, it can compete with Node and Go with regards to performance.
  5. Because it's based on and fully compatible with OpenAPI and JSON Schema, it supports a number of powerful tools, like Swagger UI.
  6. It has amazing documentation.

First time with FastAPI? Check out the following resources:

  1. Developing and Testing an Asynchronous API with FastAPI and Pytest
  2. Test-Driven Development with FastAPI and Docker

What is Vue?

Vue is an open-source JavaScript framework used for building user interfaces. It adopted some of the best practices from React and Angular. That said, compared to React and Angular, it's much more approachable, so beginners can get up and running quickly. It's also just as powerful, so it provides all the features you'll need to create modern front-end applications.

For more on Vue, along with the pros and cons of using it vs. React and Angular, review the following articles:

  1. Vue: Comparison with Other Frameworks
  2. Learn Vue by Building and Deploying a CRUD App
  3. React vs Angular vs Vue.js

First time with Vue?

  1. Take a moment to read through the Introduction from the official Vue guide.
  2. Check out the Learn Vue by Building and Deploying a CRUD App course as well.

What are we Building?

Our goal is to design a backend RESTful API, powered by Python and FastAPI, for two resources -- users and notes. The API itself should follow RESTful design principles, using the basic HTTP verbs: GET, POST, PUT, and DELETE.

We'll also set up a front-end application with Vue that interacts with the back-end API:

final app

Core functionality:

  1. Authenticated users will be able to view, add, update, and delete notes
  2. Authenticated users will also be able to view their user info and delete themselves

This tutorial mostly just deals with the happy path. Handling unhappy/exception paths is a separate exercise for the reader. Check your understanding and add proper error handling for both the frontend and backend.

FastAPI Setup

Start by creating a new project folder called "fastapi-vue" and add the following files and folders:

fastapi-vue
├── docker-compose.yml
└── services
    └── backend
        ├── Dockerfile
        ├── requirements.txt
        └── src
            └── main.py

Next, add the following code to services/backend/Dockerfile:

FROM python:3.9-buster

RUN mkdir app
WORKDIR /app

ENV PATH="${PATH}:/root/.local/bin"
ENV PYTHONPATH=.

COPY requirements.txt .
RUN pip install --upgrade pip
RUN pip install -r requirements.txt

COPY src/ .

Add the following dependencies to the services/backend/requirements.txt file:

fastapi==0.68.0
uvicorn==0.14.0

Update docker-compose.yml like so:

version: '3.8'

services:

  backend:
    build: ./services/backend
    ports:
      - 5000:5000
    volumes:
      - ./services/backend:/app
    command: uvicorn src.main:app --reload --host 0.0.0.0 --port 5000

Before we build the image, let's add a test route to services/backend/src/main.py so we can quickly test that the app was built successfully:

from fastapi import FastAPI


app = FastAPI()


@app.get("/")
def home():
    return "Hello, World!"

Build the image in your terminal:

$ docker-compose up -d --build

Once done, navigate to http://127.0.0.1:5000/ in your browser of choice. You should see:

"Hello, World!"

You can view the Swagger UI at http://localhost:5000/docs.

Next, add CORSMiddleware:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware  # NEW


app = FastAPI()

# NEW
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:8080"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.get("/")
def home():
    return "Hello, World!"

CORSMiddleware is required to make cross-origin requests -- e.g., requests that originate from a different protocol, IP address, domain name, or port -- you need to enable Cross Origin Resource Sharing (CORS). This is necessary since the frontend will run at http://localhost:8080.

Vue Setup

To get started with our frontend, we'll scaffold out a project using the Vue CLI.

Make sure you're using version 4.5.13 of the Vue CLI:

$ vue -V
@vue/cli 4.5.13

# install
$ npm install -g @vue/cli@4.5.13

Next, from the "fastapi-vue/services" folder, scaffold out a new Vue project:

$ vue create frontend

Select Default ([Vue 2] babel, eslint).

After the scaffold is up, add the router (say yes to history mode), and install the required dependencies:

$ cd frontend
$ vue add router
$ npm install --save axios@0.21.1 vuex@3.6.2 vuex-persistedstate@4.0.0 bootstrap@5.1.0

We'll discuss each of these dependencies shortly.

To serve up the Vue application locally, run:

$ npm run serve

Navigate to http://localhost:8080/ to view your app.

Kill the server.

Next, wire up the dependencies for Axios and Bootstrap in services/frontend/src/main.js:

import 'bootstrap/dist/css/bootstrap.css';
import axios from 'axios';
import Vue from 'vue';

import App from './App.vue';
import router from './router';


axios.defaults.withCredentials = true;
axios.defaults.baseURL = 'http://localhost:5000/';  // the FastAPI backend

Vue.config.productionTip = false;

new Vue({
  router,
  render: h => h(App)
}).$mount('#app');

Add a Dockerfile to "services/frontend":

FROM node:lts-alpine

WORKDIR /app

ENV PATH /app/node_modules/.bin:$PATH

RUN npm install @vue/cli@4.5.13 -g

COPY package.json .
COPY package-lock.json .
RUN npm install

CMD ["npm", "run", "serve"]

Add a frontend service to docker-compose.yml:

version: '3.8'

services:

  backend:
    build: ./services/backend
    ports:
      - 5000:5000
    volumes:
      - ./services/backend:/app
    command: uvicorn src.main:app --reload --host 0.0.0.0 --port 5000

  frontend:
    build: ./services/frontend
    volumes:
      - './services/frontend:/app'
      - '/app/node_modules'
    ports:
      - 8080:8080

Build the new image and spin up the containers:

$ docker-compose up -d --build

Ensure http://localhost:8080/ still works.

Next, update services/frontend/src/components/HelloWorld.vue like so:

<template>
  <div>
    <p>{{ msg }}</p>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  name: 'Ping',
  data() {
    return {
      msg: '',
    };
  },
  methods: {
    getMessage() {
      axios.get('/')
        .then((res) => {
          this.msg = res.data;
        })
        .catch((error) => {
          console.error(error);
        });
    },
  },
  created() {
    this.getMessage();
  },
};
</script>

Axios, which is an HTTP client, is used to send AJAX requests to the backend. In the above component, we updated the value of msg from the response from the backend.

Finally, within services/frontend/src/App.vue, remove the navigation along with the associated styles:

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}
</style>

You should now see Hello, World! in the browser at http://localhost:8080/.

Your full project structure should now look like this:

├── docker-compose.yml
└── services
    ├── backend
    │   ├── Dockerfile
    │   ├── requirements.txt
    │   └── src
    │       └── main.py
    └── frontend
        ├── .gitignore
        ├── Dockerfile
        ├── README.md
        ├── babel.config.js
        ├── package-lock.json
        ├── package.json
        ├── public
        │   ├── favicon.ico
        │   └── index.html
        └── src
            ├── App.vue
            ├── assets
            │   └── logo.png
            ├── components
            │   └── HelloWorld.vue
            ├── main.js
            ├── router
            │   └── index.js
            └── views
                ├── About.vue
                └── Home.vue

Models and Migrations

We'll be using Tortoise for our ORM (Object Relational Mapper) and Aerich for managing database migrations.

Update the backend dependencies:

aerich==0.5.5
asyncpg==0.23.0
fastapi==0.68.0
tortoise-orm==0.17.6
uvicorn==0.14.0

First, let's add a new service for Postgres to docker-compose.yml:

version: '3.8'

services:

  backend:
    build: ./services/backend
    ports:
      - 5000:5000
    environment:
      - DATABASE_URL=postgres://hello_fastapi:hello_fastapi@db:5432/hello_fastapi_dev
    volumes:
      - ./services/backend:/app
    command: uvicorn src.main:app --reload --host 0.0.0.0 --port 5000
    depends_on:
      - db

  frontend:
    build: ./services/frontend
    volumes:
      - './services/frontend:/app'
      - '/app/node_modules'
    ports:
      - 8080:8080

  db:
    image: postgres:13
    expose:
      - 5432
    environment:
      - POSTGRES_USER=hello_fastapi
      - POSTGRES_PASSWORD=hello_fastapi
      - POSTGRES_DB=hello_fastapi_dev
    volumes:
      - postgres_data:/var/lib/postgresql/data/

volumes:
  postgres_data:

Take note of the environment variables in db along with the new DATABASE_URL environment variable in the backend service.

Next, create a folder called "database" in the "services/backend/src" folder, and a new file called models.py to it:

from tortoise import fields, models


class Users(models.Model):
    id = fields.IntField(pk=True)
    username = fields.CharField(max_length=20, unique=True)
    full_name = fields.CharField(max_length=50, null=True)
    password = fields.CharField(max_length=128, null=True)
    created_at = fields.DatetimeField(auto_now_add=True)
    modified_at = fields.DatetimeField(auto_now=True)


class Notes(models.Model):
    id = fields.IntField(pk=True)
    title = fields.CharField(max_length=225)
    content = fields.TextField()
    author = fields.ForeignKeyField("models.Users", related_name="note")
    created_at = fields.DatetimeField(auto_now_add=True)
    modified_at = fields.DatetimeField(auto_now=True)

    def __str__(self):
        return f"{self.title}, {self.author_id} on {self.created_at}"

The Users and Notes classes will create two new tables in our database. Take note that the author column relates back to the user, creating a one-to-many relationship (one user can have many notes).

Create a config.py file in the "services/backend/src/database" folder:

import os


TORTOISE_ORM = {
    "connections": {"default": os.environ.get("DATABASE_URL")},
    "apps": {
        "models": {
            "models": [
                "src.database.models", "aerich.models"
            ],
            "default_connection": "default"
        }
    }
}

Here, we specified the configuration for both Tortoise and Aerich.

Put simply, we:

  1. Defined the database connection via the DATABASE_URL environment variable
  2. Registered our models, src.database.models (users and notes) and aerich.models (migration metadata)

Add a register.py file to "services/backend/src/database" as well:

from typing import Optional

from tortoise import Tortoise


def register_tortoise(
    app,
    config: Optional[dict] = None,
    generate_schemas: bool = False,
) -> None:
    @app.on_event("startup")
    async def init_orm():
        await Tortoise.init(config=config)
        if generate_schemas:
            await Tortoise.generate_schemas()

    @app.on_event("shutdown")
    async def close_orm():
        await Tortoise.close_connections()

register_tortoise is a function that will be used for configuring our application and models with Tortoise. It takes in our app, a config dict, and a generate_schema boolean.

The function will be called in main.py with our config dict:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from src.database.register import register_tortoise  # NEW
from src.database.config import TORTOISE_ORM         # NEW


app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# NEW
register_tortoise(app, config=TORTOISE_ORM, generate_schemas=False)


@app.get("/")
def home():
    return "Hello, World!"

Build the new images and spin up the containers:

$ docker-compose up -d --build

After the containers are up and running, run:

$ docker-compose exec backend aerich init -t src.database.config.TORTOISE_ORM
Success create migrate location ./migrations
Success generate config file aerich.ini

$ docker-compose exec backend aerich init-db
Success create app migrate location migrations/models
Success generate schema for app "models"

The first command told Aerich where the config dict is for initializing the connection between the models and the database. This created a services/backend/aerich.ini config file and a "services/backend/migrations" folder.

Next, we generated a migration file for our three models -- users, notes, and aerich -- inside "services/backend/migrations/models". These were applied to the database as well.

Let's copy the aerich.ini file and "migrations" folder to the container. To do so, update the Dockerfile like so:

FROM python:3.9-buster

RUN mkdir app
WORKDIR /app

ENV PATH="${PATH}:/root/.local/bin"
ENV PYTHONPATH=.

COPY requirements.txt .
RUN pip install --upgrade pip
RUN pip install -r requirements.txt

# for migrations
COPY migrations .
COPY aerich.ini .

COPY src/ .

Update:

$ docker-compose up -d --build

Now, when you make changes to the models, you can run the following commands to update the database:

$ docker-compose exec backend aerich migrate
$ docker-compose exec backend aerich upgrade

CRUD Actions

Now let's wire up the basic CRUD actions: create, read, update, and delete.

First, since we need to define schemas for serializing and deserializing our data, create two folders in "services/backend/src" called "crud" and "schemas".

To ensure our serializers can read the relationship between our models, we need to initialize the models in the main.py file:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from tortoise import Tortoise  # NEW

from src.database.register import register_tortoise
from src.database.config import TORTOISE_ORM


# enable schemas to read relationship between models
Tortoise.init_models(["src.database.models"], "models")  # NEW

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:8080"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

register_tortoise(app, config=TORTOISE_ORM, generate_schemas=False)


@app.get("/")
def home():
    return "Hello, World!"

Now, queries made on any object can get the data from the related table.

Next, in the "schemas" folder, add two files called users.py and notes.py.

services/backend/src/schemas/users.py:

from tortoise.contrib.pydantic import pydantic_model_creator

from src.database.models import Users


UserInSchema = pydantic_model_creator(
    Users, name="UserIn", exclude_readonly=True
)
UserOutSchema = pydantic_model_creator(
    Users, name="UserOut", exclude=["password", "created_at", "modified_at"]
)
UserDatabaseSchema = pydantic_model_creator(
    Users, name="User", exclude=["created_at", "modified_at"]
)

pydantic_model_creator is a Tortoise helper that allows us to create pydantic models from Tortoise models, which we'll use to create and retrieve database records. It takes in the Users model and a name. You can also exclude specific columns.

Schemas:

  1. UserInSchema is for creating new users.
  2. UserOutSchema is for retrieving user info to be used outside our application, for returning to end users.
  3. UserDatabaseSchema is for retrieving user info to be used within our application, for validating users.

services/backend/src/schemas/notes.py:

from typing import Optional

from pydantic import BaseModel
from tortoise.contrib.pydantic import pydantic_model_creator

from src.database.models import Notes


NoteInSchema = pydantic_model_creator(
    Notes, name="NoteIn", exclude=["author_id"], exclude_readonly=True)
NoteOutSchema = pydantic_model_creator(
    Notes, name="Note", exclude =[
      "modified_at", "author.password", "author.created_at", "author.modified_at"
    ]
)


class UpdateNote(BaseModel):
    title: Optional[str]
    content: Optional[str]

Schemas:

  1. NoteInSchema is for creating new notes.
  2. NoteOutSchema is for retrieving notes.
  3. UpdateNote is for updating notes.

Next, add users.py and notes.py files to the "services/backend/src/crud" folder.

services/backend/src/crud/users.py:

from fastapi import HTTPException
from passlib.context import CryptContext
from tortoise.exceptions import DoesNotExist, IntegrityError

from src.database.models import Users
from src.schemas.users import UserOutSchema


pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")


async def create_user(user) -> UserOutSchema:
    user.password = pwd_context.encrypt(user.password)

    try:
        user_obj = await Users.create(**user.dict(exclude_unset=True))
    except IntegrityError:
        raise HTTPException(status_code=401, detail=f"Sorry, that username already exists.")

    return await UserOutSchema.from_tortoise_orm(user_obj)


async def delete_user(user_id, current_user):
    try:
        db_user = await UserOutSchema.from_queryset_single(Users.get(id=user_id))
    except DoesNotExist:
        raise HTTPException(status_code=404, detail=f"User {user_id} not found")

    if db_user.id == current_user.id:
        deleted_count = await Users.filter(id=user_id).delete()
        if not deleted_count:
            raise HTTPException(status_code=404, detail=f"User {user_id} not found")
        return f"Deleted user {user_id}"

    raise HTTPException(status_code=403, detail=f"Not authorized to delete")

Here, we defined helper functions for creating and deleting users:

  1. create_user takes in a user, encrypts user.password, and then adds the user to the database.
  2. delete_user deletes a user from the database. It also protects the users by ensuring the request is initiated by a currently authenticated user.

Add the required dependencies to services/backend/requirements.txt:

aerich==0.5.5
asyncpg==0.23.0
bcrypt==3.2.0
fastapi==0.68.0
passlib==1.7.4
tortoise-orm==0.17.6
uvicorn==0.14.0

services/backend/src/crud/notes.py:

from fastapi import HTTPException
from tortoise.exceptions import DoesNotExist

from src.database.models import Notes
from src.schemas.notes import NoteOutSchema


async def get_notes():
    return await NoteOutSchema.from_queryset(Notes.all())


async def get_note(note_id) -> NoteOutSchema:
    return await NoteOutSchema.from_queryset_single(Notes.get(id=note_id))


async def create_note(note, current_user) -> NoteOutSchema:
    note_dict = note.dict(exclude_unset=True)
    note_dict["author_id"] = current_user.id
    note_obj = await Notes.create(**note_dict)
    return await NoteOutSchema.from_tortoise_orm(note_obj)


async def update_note(note_id, note, current_user) -> NoteOutSchema:
    try:
        db_note = await NoteOutSchema.from_queryset_single(Notes.get(id=note_id))
    except DoesNotExist:
        raise HTTPException(status_code=404, detail=f"Note {note_id} not found")

    if db_note.author.id == current_user.id:
        await Notes.filter(id=note_id).update(**note.dict(exclude_unset=True))
        return await NoteOutSchema.from_queryset_single(Notes.get(id=note_id))

    raise HTTPException(status_code=403, detail=f"Not authorized to update")


async def delete_note(note_id, current_user):
    try:
        db_note = await NoteOutSchema.from_queryset_single(Notes.get(id=note_id))
    except DoesNotExist:
        raise HTTPException(status_code=404, detail=f"Note {note_id} not found")

    if db_note.author.id == current_user.id:
        deleted_count = await Notes.filter(id=note_id).delete()
        if not deleted_count:
            raise HTTPException(status_code=404, detail=f"Note {note_id} not found")
        return f"Deleted note {note_id}"

    raise HTTPException(status_code=403, detail=f"Not authorized to delete")

Here, we created helper functions for implementing all the CRUD actions for the notes resource. Take note of the update_note and delete_note helpers. We added a check to ensure that the request is coming from the note author.

Your folder structure should now look like this:

├── docker-compose.yml
└── services
    ├── backend
    │   ├── Dockerfile
    │   ├── aerich.ini
    │   ├── migrations
    │   │   └── models
    │   │       └── 1_20210811013714_None.sql
    │   ├── requirements.txt
    │   └── src
    │       ├── crud
    │       │   ├── notes.py
    │       │   └── users.py
    │       ├── database
    │       │   ├── config.py
    │       │   ├── models.py
    │       │   └── register.py
    │       ├── main.py
    │       └── schemas
    │           ├── notes.py
    │           └── users.py
    └── frontend
        ├── .gitignore
        ├── Dockerfile
        ├── README.md
        ├── babel.config.js
        ├── package-lock.json
        ├── package.json
        ├── public
        │   ├── favicon.ico
        │   └── index.html
        └── src
            ├── App.vue
            ├── assets
            │   └── logo.png
            ├── components
            │   └── HelloWorld.vue
            ├── main.js
            ├── router
            │   └── index.js
            └── views
                ├── About.vue
                └── Home.vue

This is a good time to stop, review what you've accomplished thus far, and wire up pytest to test the CRUD helpers. Need help? Review Developing and Testing an Asynchronous API with FastAPI and Pytest.

JWT Authentication

Before we add the route handlers, let's wire up authentication to protect specific routes.

To start, we need to create a few pydantic models in a new file called token.py in the "services/backend/src/schemas" folder:

from typing import Optional

from pydantic import BaseModel


class TokenData(BaseModel):
    username: Optional[str] = None


class Status(BaseModel):
    message: str

We defined two schemas:

  1. TokenData is for ensuring the username from the token is a string.
  2. Status is for sending status messages back to the end user.

Create another folder called "auth" in the "services/backend/src" folder. Then, add two new files to it as well called jwthandler.py and users.py.

services/backend/src/auth/jwthandler.py:

import os
from datetime import datetime, timedelta
from typing import Optional

from fastapi import Depends, HTTPException, Request
from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
from fastapi.security import OAuth2
from fastapi.security.utils import get_authorization_scheme_param
from jose import JWTError, jwt
from tortoise.exceptions import DoesNotExist

from src.schemas.token import TokenData
from src.schemas.users import UserOutSchema
from src.database.models import Users


SECRET_KEY = os.environ.get("SECRET_KEY")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30


class OAuth2PasswordBearerCookie(OAuth2):
    def __init__(
        self,
        token_url: str,
        scheme_name: str = None,
        scopes: dict = None,
        auto_error: bool = True,
    ):
        if not scopes:
            scopes = {}
        flows = OAuthFlowsModel(password={"tokenUrl": token_url, "scopes": scopes})
        super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)

    async def __call__(self, request: Request) -> Optional[str]:
        authorization: str = request.cookies.get("Authorization")
        scheme, param = get_authorization_scheme_param(authorization)

        if not authorization or scheme.lower() != "bearer":
            if self.auto_error:
                raise HTTPException(
                    status_code=401,
                    detail="Not authenticated",
                    headers={"WWW-Authenticate": "Bearer"},
                )
            else:
                return None

        return param


security = OAuth2PasswordBearerCookie(token_url="/login")


def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
    to_encode = data.copy()

    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)

    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

    return encoded_jwt


async def get_current_user(token: str = Depends(security)):
    credentials_exception = HTTPException(
        status_code=401,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )

    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        token_data = TokenData(username=username)
    except JWTError:
        raise credentials_exception

    try:
        user = await UserOutSchema.from_queryset_single(
            Users.get(username=token_data.username)
        )
    except DoesNotExist:
        raise credentials_exception

    return user

Notes:

  1. OAuth2PasswordBearerCookie is a class that inherits from the OAuth2 class that is used for reading the cookie sent in the request header for protected routes. It ensures that the cookie is present and then returns the token from the cookie.
  2. The create_access_token function takes in the user's username, encodes it with the expiring time, and generates a token from it.
  3. get_current_user decodes the token and validates the user.

python-jose is used for encoding and decoding the JWT token. Add the package to the requirements file:

aerich==0.5.5
asyncpg==0.23.0
bcrypt==3.2.0
fastapi==0.68.0
passlib==1.7.4
python-jose==3.3.0
tortoise-orm==0.17.6
uvicorn==0.14.0

Add the SECRET_KEY environment variable to docker-compose.yml:

version: '3.8'

services:

  backend:
    build: ./services/backend
    ports:
      - 5000:5000
    environment:
      - DATABASE_URL=postgres://hello_fastapi:hello_fastapi@db:5432/hello_fastapi_dev
      - SECRET_KEY=09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7
    volumes:
      - ./services/backend:/app
    command: uvicorn src.main:app --reload --host 0.0.0.0 --port 5000
    depends_on:
      - db

  frontend:
    build: ./services/frontend
    volumes:
      - './services/frontend:/app'
      - '/app/node_modules'
    ports:
      - 8080:8080

  db:
    image: postgres:13
    expose:
      - 5432
    environment:
      - POSTGRES_USER=hello_fastapi
      - POSTGRES_PASSWORD=hello_fastapi
      - POSTGRES_DB=hello_fastapi_dev
    volumes:
      - postgres_data:/var/lib/postgresql/data/

volumes:
  postgres_data:

services/backend/src/auth/users.py:

from fastapi import HTTPException, Depends, status
from fastapi.security import OAuth2PasswordRequestForm
from passlib.context import CryptContext
from tortoise.exceptions import DoesNotExist

from src.database.models import Users
from src.schemas.users import UserDatabaseSchema


pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")


def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)


def get_password_hash(password):
    return pwd_context.hash(password)


async def get_user(username: str):
    return await UserDatabaseSchema.from_queryset_single(Users.get(username=username))


async def validate_user(user: OAuth2PasswordRequestForm = Depends()):
    try:
        db_user = await get_user(user.username)
    except DoesNotExist:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
        )

    if not verify_password(user.password, db_user.password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
        )

    return db_user

Notes:

  • validate_user is used in to verify a user when they log in. If either the username or password are incorrect, it throws a 401_UNAUTHORIZED error back to the user.

Finally, let's update the CRUD helpers so that they use the Status pydantic model:

class Status(BaseModel):
    message: str

services/backend/src/crud/users.py:

from fastapi import HTTPException
from passlib.context import CryptContext
from tortoise.exceptions import DoesNotExist, IntegrityError

from src.database.models import Users
from src.schemas.token import Status  # NEW
from src.schemas.users import UserOutSchema


pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")


async def create_user(user) -> UserOutSchema:
    user.password = pwd_context.encrypt(user.password)

    try:
        user_obj = await Users.create(**user.dict(exclude_unset=True))
    except IntegrityError:
        raise HTTPException(status_code=401, detail=f"Sorry, that username already exists.")

    return await UserOutSchema.from_tortoise_orm(user_obj)


async def delete_user(user_id, current_user) -> Status:  # UPDATED
    try:
        db_user = await UserOutSchema.from_queryset_single(Users.get(id=user_id))
    except DoesNotExist:
        raise HTTPException(status_code=404, detail=f"User {user_id} not found")

    if db_user.id == current_user.id:
        deleted_count = await Users.filter(id=user_id).delete()
        if not deleted_count:
            raise HTTPException(status_code=404, detail=f"User {user_id} not found")
        return Status(message=f"Deleted user {user_id}")  # UPDATED

    raise HTTPException(status_code=403, detail=f"Not authorized to delete")

services/backend/src/crud/notes.py:

from fastapi import HTTPException
from tortoise.exceptions import DoesNotExist

from src.database.models import Notes
from src.schemas.notes import NoteOutSchema
from src.schemas.token import Status  # NEW


async def get_notes():
    return await NoteOutSchema.from_queryset(Notes.all())


async def get_note(note_id) -> NoteOutSchema:
    return await NoteOutSchema.from_queryset_single(Notes.get(id=note_id))


async def create_note(note, current_user) -> NoteOutSchema:
    note_dict = note.dict(exclude_unset=True)
    note_dict["author_id"] = current_user.id
    note_obj = await Notes.create(**note_dict)
    return await NoteOutSchema.from_tortoise_orm(note_obj)


async def update_note(note_id, note, current_user) -> NoteOutSchema:
    try:
        db_note = await NoteOutSchema.from_queryset_single(Notes.get(id=note_id))
    except DoesNotExist:
        raise HTTPException(status_code=404, detail=f"Note {note_id} not found")

    if db_note.author.id == current_user.id:
        await Notes.filter(id=note_id).update(**note.dict(exclude_unset=True))
        return await NoteOutSchema.from_queryset_single(Notes.get(id=note_id))

    raise HTTPException(status_code=403, detail=f"Not authorized to update")


async def delete_note(note_id, current_user) -> Status:  # UPDATED
    try:
        db_note = await NoteOutSchema.from_queryset_single(Notes.get(id=note_id))
    except DoesNotExist:
        raise HTTPException(status_code=404, detail=f"Note {note_id} not found")

    if db_note.author.id == current_user.id:
        deleted_count = await Notes.filter(id=note_id).delete()
        if not deleted_count:
            raise HTTPException(status_code=404, detail=f"Note {note_id} not found")
        return Status(message=f"Deleted note {note_id}")  # UPDATED

    raise HTTPException(status_code=403, detail=f"Not authorized to delete")

Routing

With the pydantic models, CRUD helpers, and JWT authentication set up, we can now glue everything together with the route handlers.

Create a "routes" folder in our "src" folder and add two files, users.py and notes.py.

users.py:

from datetime import timedelta

from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from fastapi.security import OAuth2PasswordRequestForm

from tortoise.contrib.fastapi import HTTPNotFoundError

import src.crud.users as crud
from src.auth.users import validate_user
from src.schemas.token import Status
from src.schemas.users import UserInSchema, UserOutSchema

from src.auth.jwthandler import (
    create_access_token,
    get_current_user,
    ACCESS_TOKEN_EXPIRE_MINUTES,
)


router = APIRouter()


@router.post("/register", response_model=UserOutSchema)
async def create_user(user: UserInSchema) -> UserOutSchema:
    return await crud.create_user(user)


@router.post("/login")
async def login(user: OAuth2PasswordRequestForm = Depends()):
    user = await validate_user(user)

    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )

    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    token = jsonable_encoder(access_token)
    content = {"message": "You've successfully logged in. Welcome back!"}
    response = JSONResponse(content=content)
    response.set_cookie(
        "Authorization",
        value=f"Bearer {token}",
        httponly=True,
        max_age=1800,
        expires=1800,
        samesite="Lax",
        secure=False,
    )

    return response


@router.get(
    "/users/whoami", response_model=UserOutSchema, dependencies=[Depends(get_current_user)]
)
async def read_users_me(current_user: UserOutSchema = Depends(get_current_user)):
    return current_user


@router.delete(
    "/user/{user_id}",
    response_model=Status,
    responses={404: {"model": HTTPNotFoundError}},
    dependencies=[Depends(get_current_user)],
)
async def delete_user(
    user_id: int, current_user: UserOutSchema = Depends(get_current_user)
) -> Status:
    return await crud.delete_user(user_id, current_user)

What's happening here?

  1. get_current_user is attached to read_users_me and delete_user in order to protect the routes. Unless the user is logged in as current_user, they won't be able to access them.
  2. /register leverages the crud.create_user helper to create a new user and add it to the database.
  3. /login takes in a user via form data from OAuth2PasswordRequestForm containing the username and password. It then calls the validate_user function with the user or throws an exception if None. An access token is generated from the create_access_token function and then attached to the response header as a cookie.
  4. /users/whoami takes in get_current_user and sends back the results as a response.
  5. /user/{user_id} is a dynamic route that takes in the user_id and sends it to the crud.delete_user helper with the results from current_user.

OAuth2PasswordRequestForm requires Python-Multipart. Add it to services/backend/requirements.txt:

aerich==0.5.5
asyncpg==0.23.0
bcrypt==3.2.0
fastapi==0.68.0
passlib==1.7.4
python-jose==3.3.0
python-multipart==0.0.5
tortoise-orm==0.17.6
uvicorn==0.14.0

After users successfully authenticate, a cookie is sent back, via Set-Cookie, in the response header. When users make subsequent requests, it's attached to the request header.

Take note of:

response.set_cookie(
    "Authorization",
    value=f"Bearer {token}",
    httponly=True,
    max_age=1800,
    expires=1800,
    samesite="Lax",
    secure=False,
)

Notes:

  1. The name of the cookie is Authorization with a value of Bearer {token}, with token being the actual token. It expires after 1800 seconds (30 minutes).
  2. httponly is set to True for security purposes so that client-side scripts won't be able to access the cookie. This helps prevent Cross Site Scripting (XSS) attacks.
  3. With samesite set to Lax, the browser only sends cookies on some HTTP requests. This helps prevent Cross Site Request Forgery (CSRF) attacks.
  4. Finally, secure is set to False since we'll be testing locally, without HTTPS. Make sure to set this to True in production.

notes.py:

from typing import List

from fastapi import APIRouter, Depends, HTTPException
from tortoise.contrib.fastapi import HTTPNotFoundError
from tortoise.exceptions import DoesNotExist

import src.crud.notes as crud
from src.auth.jwthandler import get_current_user
from src.schemas.notes import NoteOutSchema, NoteInSchema, UpdateNote
from src.schemas.token import Status
from src.schemas.users import UserOutSchema


router = APIRouter()


@router.get(
    "/notes",
    response_model=List[NoteOutSchema],
    dependencies=[Depends(get_current_user)],
)
async def get_notes():
    return await crud.get_notes()


@router.get(
    "/note/{note_id}",
    response_model=NoteOutSchema,
    dependencies=[Depends(get_current_user)],
)
async def get_note(note_id: int) -> NoteOutSchema:
    try:
        return await crud.get_note(note_id)
    except DoesNotExist:
        raise HTTPException(
            status_code=404,
            detail="Note does not exist",
        )


@router.post(
    "/notes", response_model=NoteOutSchema, dependencies=[Depends(get_current_user)]
)
async def create_note(
    note: NoteInSchema, current_user: UserOutSchema = Depends(get_current_user)
) -> NoteOutSchema:
    return await crud.create_note(note, current_user)


@router.patch(
    "/note/{note_id}",
    dependencies=[Depends(get_current_user)],
    response_model=NoteOutSchema,
    responses={404: {"model": HTTPNotFoundError}},
)
async def update_note(
    note_id: int,
    note: UpdateNote,
    current_user: UserOutSchema = Depends(get_current_user),
) -> NoteOutSchema:
    return await crud.update_note(note_id, note, current_user)


@router.delete(
    "/note/{note_id}",
    response_model=Status,
    responses={404: {"model": HTTPNotFoundError}},
    dependencies=[Depends(get_current_user)],
)
async def delete_note(
    note_id: int, current_user: UserOutSchema = Depends(get_current_user)
):
    return await crud.delete_note(note_id, current_user)

Review this on your own.

Finally, we need to wire up our routes in main.py:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from tortoise import Tortoise

from src.database.register import register_tortoise
from src.database.config import TORTOISE_ORM


# enable schemas to read relationship between models
Tortoise.init_models(["src.database.models"], "models")

"""
import 'from src.routes import users, notes' must be after 'Tortoise.init_models'
why?
https://stackoverflow.com/questions/65531387/tortoise-orm-for-python-no-returns-relations-of-entities-pyndantic-fastapi
"""
from src.routes import users, notes

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:8080"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
app.include_router(users.router)
app.include_router(notes.router)

register_tortoise(app, config=TORTOISE_ORM, generate_schemas=False)


@app.get("/")
def home():
    return "Hello, World!"

Update the images to install the new dependencies:

$ docker-compose up -d --build

Navigate to http://localhost:5000/docs to view the Swagger UI:

Swagger UI

You can now manually test each route.

What should you test?

RouteMethodHappy PathUnhappy Path(s)
/registerPOSTYou can register a new userDuplicate username, missing username or password fields
/loginPOSTYou can log a userIncorrect username or password
/users/whoamiGETReturns user info when authenticatedNo Authorization cookie or invalid token
/user/{user_id}DELETEYou can delete a user when authenticated and you're trying to delete the current userUser not found, user exists but not authorized to delete
/notesGETYou can get all notes when authenticatedNot authenticated
/notesPOSTYou can add a note when authenticatedNot authenticated
/note/{note_id}GETYou can get the note when authenticated and it existsNot authenticated, authenticated but the note doesn't exist
/note/{note_id}DELETEYou can delete the note when authenticated, the note exists, and the current user created the noteNot authenticated, authenticated but the note doesn't exist, not exists but not authorized to delete
/note/{note_id}PATCHYou can update the note when authenticated, the note exists, and the current user created the noteNot authenticated, authenticated but the note doesn't exist, not exists but not authorized to update

That's a lot of tedious manual testing. It's a good idea to add automated tests with pytest. Again, review Developing and Testing an Asynchronous API with FastAPI and Pytest for help with this.

With that, let's turn our attention to the frontend.

Vuex

Vuex is Vue's state management pattern and library. It manages state globally. In Vuex, mutations, which are called by actions, are used to change state.

vuex-persistedstate let's you persist Vuex state to local storage so that you can rehydrate the Vuex state after page reloads.

Add a new folder to "services/frontend/src" called "store". Within "store", add the following files and folders:

services/frontend/src/store
├── index.js
└── modules
    ├── notes.js
    └── users.js

services/frontend/src/store/index.js:

import createPersistedState from "vuex-persistedstate";
import Vue from 'vue';
import Vuex from 'vuex';

import notes from './modules/notes';
import users from './modules/users';


Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    notes,
    users,
  },
  plugins: [createPersistedState()]
});

Here, we created a new Vuex Store with two modules, notes.js and users.js. CreatePersistedState is added as a plugin, so that when we reload the browser, the state of each module won't be lost.

services/frontend/src/store/modules/notes.js:

import axios from 'axios';

const state = {
  notes: null,
  note: null
};

const getters = {
  stateNotes: state => state.notes,
  stateNote: state => state.note,
};

const actions = {
  async createNote({dispatch}, note) {
    await axios.post('notes', note);
    await dispatch('getNotes');
  },
  async getNotes({commit}) {
    let {data} = await axios.get('notes');
    commit('setNotes', data);
  },
  async viewNote({commit}, id) {
    let {data} = await axios.get(`note/${id}`);
    commit('setNote', data);
  },
  // eslint-disable-next-line no-empty-pattern
  async updateNote({}, note) {
    await axios.patch(`note/${note.id}`, note.form);
  },
  // eslint-disable-next-line no-empty-pattern
  async deleteNote({}, id) {
    await axios.delete(`note/${id}`);
  }
};

const mutations = {
  setNotes(state, notes){
    state.notes = notes;
  },
  setNote(state, note){
    state.note = note;
  },
};

export default {
  state,
  getters,
  actions,
  mutations
};

Notes:

  1. state - both note and notes default to null. They'll be updated to an object and an array of objects, respectively.
  2. getters - retrieves the values of state.note and state.notes.
  3. actions - each of the actions make an HTTP call via Axios and then a few of them perform a side effect -- e.g., call the relevant mutation to update state or a different action.
  4. mutations - both make changes to the state, which update state.note and state.notes.

services/frontend/src/store/modules/users.js:

import axios from 'axios';

const state = {
  user: null,
};

const getters = {
  isAuthenticated: state => !!state.user,
  stateUser: state => state.user,
};

const actions = {
  async register({dispatch}, form) {
    await axios.post('register', form);
    let UserForm = new FormData();
    UserForm.append('username', form.username);
    UserForm.append('password', form.password);
    await dispatch('logIn', UserForm);
  },
  async logIn({dispatch}, user) {
    await axios.post('login', user);
    await dispatch('viewMe');
  },
  async viewMe({commit}) {
    let {data} = await axios.get('users/whoami');
    await commit('setUser', data);
  },
  // eslint-disable-next-line no-empty-pattern
  async deleteUser({}, id) {
    await axios.delete(`user/${id}`);
  },
  async logOut({commit}) {
    let user = null;
    commit('logout', user);
  }
};

const mutations = {
  setUser(state, username) {
    state.user = username;
  },
  logout(state, user){
    state.user = user;
  },
};

export default {
  state,
  getters,
  actions,
  mutations
};

Notes:

  1. isAuthenticated - returns true if state.user is not null and false otherwise.
  2. stateUser - returns the value of state.user.
  3. register - sends a POST request to the /register endpoint we created in the backend, creates a FormData instance, and dispatches it to the logIn action to log the registered user in.

Finally, wire up the store to the root instance in services/frontend/src/main.js:

import 'bootstrap/dist/css/bootstrap.css';
import axios from 'axios';
import Vue from 'vue';

import App from './App.vue';
import router from './router';
import store from './store';


axios.defaults.withCredentials = true;
axios.defaults.baseURL = 'http://localhost:5000/';  // the FastAPI backend

Vue.config.productionTip = false;

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app');

Components, Views, and Routes

Next, we'll start adding the components and views.

Components

NavBar

services/frontend/src/components/NavBar.vue:

<template>
  <header>
    <nav class="navbar navbar-expand-md navbar-dark bg-dark">
      <div class="container">
        <a class="navbar-brand" href="/">FastAPI + Vue</a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
          <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarCollapse">
          <ul v-if="isLoggedIn" class="navbar-nav me-auto mb-2 mb-md-0">
            <li class="nav-item">
              <router-link class="nav-link" to="/">Home</router-link>
            </li>
            <li class="nav-item">
              <router-link class="nav-link" to="/dashboard">Dashboard</router-link>
            </li>
            <li class="nav-item">
              <router-link class="nav-link" to="/profile">My Profile</router-link>
            </li>
            <li class="nav-item">
              <a class="nav-link" @click="logout">Log Out</a>
            </li>
          </ul>
          <ul v-else class="navbar-nav me-auto mb-2 mb-md-0">
            <li class="nav-item">
              <router-link class="nav-link" to="/">Home</router-link>
            </li>
            <li class="nav-item">
              <router-link class="nav-link" to="/register">Register</router-link>
            </li>
            <li class="nav-item">
              <router-link class="nav-link" to="/login">Log In</router-link>
            </li>
          </ul>
        </div>
      </div>
    </nav>
  </header>
</template>

<script>
export default {
  name: 'NavBar',
  computed: {
    isLoggedIn: function() {
      return this.$store.getters.isAuthenticated;
    }
  },
  methods: {
    async logout () {
      await this.$store.dispatch('logOut');
      this.$router.push('/login');
    }
  },
}
</script>

<style scoped>
a {
  cursor: pointer;
}
</style>

The NavBar is used for navigating to other pages in the application. The isLoggedIn property is used to check if a user is logged in from the store. If they are logged in, the dashboard and profile is accessible to them, including the logout link.

The logout function dispatches the logOut action and redirects the user to the /login route.

App

Next, let's add the NavBar component to the main App component.

services/frontend/src/App.vue:

<template>
  <div id="app">
    <NavBar />
    <div class="main container">
      <router-view/>
    </div>
  </div>
</template>

<script>
// @ is an alias to /src
import NavBar from '@/components/NavBar.vue'
export default {
  components: {
    NavBar
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
.main {
  padding-top: 5em;
}
</style>

You should now be able to see the new nav bar at http://localhost:8080/.

Views

Home

services/frontend/src/views/Home.vue:

<template>
  <section>
    <p>This site is built with FastAPI and Vue.</p>

    <div v-if="isLoggedIn" id="logout">
      <p id="logout">Click <a href="/dashboard">here</a> to view all notes.</p>
    </div>
    <p v-else>
      <span><a href="/register">Register</a></span>
      <span> or </span>
      <span><a href="/login">Log In</a></span>
    </p>
  </section>
</template>
<script>

export default {
  name: 'Home',
  computed : {
    isLoggedIn: function() {
      return this.$store.getters.isAuthenticated;
    }
  },
}
</script>

Here, the end user is displayed either a link to all notes or links to sign up/in based on the value of the isLoggedIn property.

Next, wire up the view to our routes in services/frontend/src/router/index.js:

import Vue from 'vue';
import VueRouter from 'vue-router';

import Home from '@/views/Home.vue';

Vue.use(VueRouter);

const routes = [
  {
    path: '/',
    name: "Home",
    component: Home,
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes,
});

export default router;

Navigate to http://localhost:8080/. You should see:

home

Register

services/frontend/src/views/Register.vue:

<template>
  <section>
    <form @submit.prevent="submit">
      <div class="mb-3">
        <label for="username" class="form-label">Username:</label>
        <input type="text" name="username" v-model="user.username" class="form-control" />
      </div>
      <div class="mb-3">
        <label for="full_name" class="form-label">Full Name:</label>
        <input type="text" name="full_name" v-model="user.full_name" class="form-control" />
      </div>
      <div class="mb-3">
        <label for="password" class="form-label">Password:</label>
        <input type="password" name="password" v-model="user.password" class="form-control" />
      </div>
      <button type="submit" class="btn btn-primary">Submit</button>
    </form>
  </section>
</template>

<script>
import { mapActions } from 'vuex';
export default {
  name: 'Register',
  data() {
    return {
      user: {
        username: '',
        full_name: '',
        password: '',
      },
    };
  },
  methods: {
    ...mapActions(['register']),
    async submit() {
      try {
        await this.register(this.user);
        this.$router.push('/dashboard');
      } catch (error) {
        throw 'Username already exists. Please try again.';
      }
    },
  },
};
</script>

The form takes in the username, full name, and password, all of which are properties on the user object. The Register action is mapped (imported) into the component via mapActions. this.Register is then called and passed the user object. If the result is successful, the user is then redirected to the /dashboard.

Update the router:

import Vue from 'vue';
import VueRouter from 'vue-router';

import Home from '@/views/Home.vue';
import Register from '@/views/Register.vue';

Vue.use(VueRouter);

const routes = [
  {
    path: '/',
    name: "Home",
    component: Home,
  },
  {
    path: '/register',
    name: 'Register',
    component: Register,
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes,
});

export default router;

Test http://localhost:8080/register, ensuring that you can register a new user.

register

Login

services/frontend/src/views/Login.vue:

<template>
  <section>
    <form @submit.prevent="submit">
      <div class="mb-3">
        <label for="username" class="form-label">Username:</label>
        <input type="text" name="username" v-model="form.username" class="form-control" />
      </div>
      <div class="mb-3">
        <label for="password" class="form-label">Password:</label>
        <input type="password" name="password" v-model="form.password" class="form-control" />
      </div>
      <button type="submit" class="btn btn-primary">Submit</button>
    </form>
  </section>
</template>

<script>
import { mapActions } from 'vuex';
export default {
  name: 'Login',
  data() {
    return {
      form: {
        username: '',
        password:'',
      }
    };
  },
  methods: {
    ...mapActions(['logIn']),
    async submit() {
      const User = new FormData();
      User.append('username', this.form.username);
      User.append('password', this.form.password);
      await this.logIn(User);
      this.$router.push('/dashboard');
    }
  }
}
</script>

On submission, the logIn action is called. On success, the user is redirected to /dashboard.

Update the router:

import Vue from 'vue';
import VueRouter from 'vue-router';

import Home from '@/views/Home.vue';
import Login from '@/views/Login.vue';
import Register from '@/views/Register.vue';

Vue.use(VueRouter);

const routes = [
  {
    path: '/',
    name: "Home",
    component: Home,
  },
  {
    path: '/register',
    name: 'Register',
    component: Register,
  },
  {
    path: '/login',
    name: 'Login',
    component: Login,
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes,
});

export default router;

Test http://localhost:8080/login, ensuring that you can log a registered user in.

Dashboard

services/frontend/src/views/Dashboard.vue:

<template>
  <div>
    <section>
      <h1>Add new note</h1>
      <hr/><br/>

      <form @submit.prevent="submit">
        <div class="mb-3">
          <label for="title" class="form-label">Title:</label>
          <input type="text" name="title" v-model="form.title" class="form-control" />
        </div>
        <div class="mb-3">
          <label for="content" class="form-label">Content:</label>
          <textarea
            name="content"
            v-model="form.content"
            class="form-control"
          ></textarea>
        </div>
        <button type="submit" class="btn btn-primary">Submit</button>
      </form>
    </section>

    <br/><br/>

    <section>
      <h1>Notes</h1>
      <hr/><br/>

      <div v-if="notes.length">
        <div v-for="note in notes" :key="note.id" class="notes">
          <div class="card" style="width: 18rem;">
            <div class="card-body">
              <ul>
                <li><strong>Note Title:</strong> {{ note.title }}</li>
                <li><strong>Author:</strong> {{ note.author.username }}</li>
                <li><router-link :to="{name: 'Note', params:{id: note.id}}">View</router-link></li>
              </ul>
            </div>
          </div>
          <br/>
        </div>
      </div>

      <div v-else>
        <p>Nothing to see. Check back later.</p>
      </div>
    </section>
  </div>
</template>

<script>
import { mapGetters, mapActions } from 'vuex';
export default {
  name: 'Dashboard',
  data() {
    return {
      form: {
        title: '',
        content: '',
      },
    };
  },
  created: function() {
    return this.$store.dispatch('getNotes');
  },
  computed: {
    ...mapGetters({ notes: 'stateNotes'}),
  },
  methods: {
    ...mapActions(['createNote']),
    async submit() {
      await this.createNote(this.form);
    },
  },
};
</script>

The dashboard displays all notes from the API and also allows users to create new notes. Take note of:

<router-link :to="{name: 'Note', params:{id: note.id}}">View</router-link>

We'll configure the route and view here shortly, but the key thing to take away is that the route takes in the note ID and sends the user to the corresponding route -- i.e., note/1, note/2, note/10, note/101, and so forth.

The created function is called during the creation of the component, which hooks into the component lifecycle. In it, we called the mapped getNotes action.

Router:

import Vue from 'vue';
import VueRouter from 'vue-router';

import Dashboard from '@/views/Dashboard.vue';
import Home from '@/views/Home.vue';
import Login from '@/views/Login.vue';
import Register from '@/views/Register.vue';

Vue.use(VueRouter);

const routes = [
  {
    path: '/',
    name: "Home",
    component: Home,
  },
  {
    path: '/register',
    name: 'Register',
    component: Register,
  },
  {
    path: '/login',
    name: 'Login',
    component: Login,
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: Dashboard,
    meta: {requiresAuth: true},
  },
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes,
});

export default router;

Ensure that after you register or log in, you are redirected to the dashboard and that it's now displayed correctly:

dashboard

You should be able to add a note as well:

add_note

Profile

services/frontend/src/views/Profile.vue:

<template>
  <section>
    <h1>Your Profile</h1>
    <hr/><br/>
    <div>
      <p><strong>Full Name:</strong> <span>{{ user.full_name }}</span></p>
      <p><strong>Username:</strong> <span>{{ user.username }}</span></p>
      <p><button @click="deleteAccount()" class="btn btn-primary">Delete Account</button></p>
    </div>
  </section>
</template>

<script>
import { mapGetters, mapActions } from 'vuex';
export default {
  name: 'Profile',
  created: function() {
    return this.$store.dispatch('viewMe');
  },
  computed: {
    ...mapGetters({user: 'stateUser' }),
  },
  methods: {
    ...mapActions(['deleteUser']),
    async deleteAccount() {
      try {
        await this.deleteUser(this.user.id);
        await this.$store.dispatch('logOut');
        this.$router.push('/');
      } catch (error) {
        console.error(error);
      }
    }
  },
}
</script>

The "Delete Account" button calls deleteUser, which sends the user.id to the deleteUser action, logs the user out, and then redirects the user back to the home page.

Router:

import Vue from 'vue';
import VueRouter from 'vue-router';

import Dashboard from '@/views/Dashboard';
import Home from '@/views/Home.vue';
import Login from '@/views/Login';
import Profile from '@/views/Profile';
import Register from '@/views/Register';

Vue.use(VueRouter);

const routes = [
  {
    path: '/',
    name: "Home",
    component: Home,
  },
  {
    path: '/register',
    name: 'Register',
    component: Register,
  },
  {
    path: '/login',
    name: 'Login',
    component: Login,
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: Dashboard,
    meta: {requiresAuth: true},
  },
  {
    path: '/profile',
    name: 'Profile',
    component: Profile,
    meta: {requiresAuth: true},
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes,
});

export default router;

Ensure that you can view your profile at http://localhost:8080/profile. Test out the delete functionality as well.

profile

Note

services/frontend/src/views/Note.vue:

<template>
  <div v-if="note">
    <p><strong>Title:</strong> {{ note.title }}</p>
    <p><strong>Content:</strong> {{ note.content }}</p>
    <p><strong>Author:</strong> {{ note.author.username }}</p>

    <div v-if="user.id === note.author.id">
      <p><router-link :to="{name: 'EditNote', params:{id: note.id}}" class="btn btn-primary">Edit</router-link></p>
      <p><button @click="removeNote()" class="btn btn-secondary">Delete</button></p>
    </div>
  </div>
</template>


<script>
import { mapGetters, mapActions } from 'vuex';
export default {
  name: 'Note',
  props: ['id'],
  async created() {
    try {
      await this.viewNote(this.id);
    } catch (error) {
      console.error(error);
      this.$router.push('/dashboard');
    }
  },
  computed: {
    ...mapGetters({ note: 'stateNote', user: 'stateUser'}),
  },
  methods: {
    ...mapActions(['viewNote', 'deleteNote']),
    async removeNote() {
      try {
        await this.deleteNote(this.id);
        this.$router.push('/dashboard');
      } catch (error) {
        console.error(error);
      }
    }
  },
};
</script>

This view loads the note details of any note ID passed to it from it's route as a prop.

In the created lifecycle hook, we passed the id from the props to the viewNote action from the store. stateUser and stateNote are mapped into the component, via mapGetters, as user and note, respectively. The "Delete" button triggers the deleteNote method, which, in turn, calls the deleteNote action and redirects the user back to the /dashboard route.

We used an if statement to display the "Edit" and "Delete" buttons only if the note.author is the same as the logged in user.

Router:

import Vue from 'vue';
import VueRouter from 'vue-router';

import Dashboard from '@/views/Dashboard';
import Home from '@/views/Home.vue';
import Login from '@/views/Login';
import Note from '@/views/Note';
import Profile from '@/views/Profile';
import Register from '@/views/Register';

Vue.use(VueRouter);

const routes = [
  {
    path: '/',
    name: "Home",
    component: Home,
  },
  {
    path: '/register',
    name: 'Register',
    component: Register,
  },
  {
    path: '/login',
    name: 'Login',
    component: Login,
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: Dashboard,
    meta: {requiresAuth: true},
  },
  {
    path: '/profile',
    name: 'Profile',
    component: Profile,
    meta: {requiresAuth: true},
  },
  {
    path: '/note/:id',
    name: 'Note',
    component: Note,
    meta: {requiresAuth: true},
    props: true,
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes,
});

export default router;

Since, this route is dyanmic, we set props to true so that the note ID is passed to the view as a prop from the URL.

From the dashboard, click the link to view a new note. Make sure the "Edit" and "Delete" buttons are displayed only if the logged in user is the note creator:

note

Also, make sure you can delete a note.

EditNote

services/frontend/src/views/EditNote.vue:

<template>
  <section>
    <h1>Edit note</h1>
    <hr/><br/>

    <form @submit.prevent="submit">
      <div class="mb-3">
        <label for="title" class="form-label">Title:</label>
        <input type="text" name="title" v-model="form.title" class="form-control" />
      </div>
      <div class="mb-3">
        <label for="content" class="form-label">Content:</label>
        <textarea
          name="content"
          v-model="form.content"
          class="form-control"
        ></textarea>
      </div>
      <button type="submit" class="btn btn-primary">Submit</button>
    </form>
  </section>
</template>

<script>
import { mapGetters, mapActions } from 'vuex';
export default {
  name: 'EditNote',
  props: ['id'],
  data() {
    return {
      form: {
        title: '',
        content: '',
      },
    };
  },
  created: function() {
    this.GetNote();
  },
  computed: {
    ...mapGetters({ note: 'stateNote' }),
  },
  methods: {
    ...mapActions(['updateNote', 'viewNote']),
    async submit() {
    try {
      let note = {
        id: this.id,
        form: this.form,
      };
      await this.updateNote(note);
      this.$router.push({name: 'Note', params:{id: this.note.id}});
    } catch (error) {
      console.log(error);
    }
    },
    async GetNote() {
      try {
        await this.viewNote(this.id);
        this.form.title = this.note.title;
        this.form.content = this.note.content;
      } catch (error) {
        console.error(error);
        this.$router.push('/dashboard');
      }
    }
  },
};
</script>

This view displays a pre-loaded form with the note title and content for the author to edit and update. Similar to the Note view, the id of the note is passed from the router object to the page as a prop.

The getNote method is used to load the form with the note info. It passes the id to the viewNote action and uses the note getter values to fill the form. While the component is being created, the getNote function is called.

Router:

import Vue from 'vue';
import VueRouter from 'vue-router';

import Dashboard from '@/views/Dashboard';
import EditNote from '@/views/EditNote';
import Home from '@/views/Home.vue';
import Login from '@/views/Login';
import Note from '@/views/Note';
import Profile from '@/views/Profile';
import Register from '@/views/Register';

Vue.use(VueRouter);

const routes = [
  {
    path: '/',
    name: "Home",
    component: Home,
  },
  {
    path: '/register',
    name: 'Register',
    component: Register,
  },
  {
    path: '/login',
    name: 'Login',
    component: Login,
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: Dashboard,
    meta: {requiresAuth: true},
  },
  {
    path: '/profile',
    name: 'Profile',
    component: Profile,
    meta: {requiresAuth: true},
  },
  {
    path: '/note/:id',
    name: 'Note',
    component: Note,
    meta: {requiresAuth: true},
    props: true,
  },
  {
    path: '/note/:id',
    name: 'EditNote',
    component: EditNote,
    meta: {requiresAuth: true},
    props: true,
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes,
});

export default router;

Manually test this out before moving on.

Handling Unauthorized Users and Expired Tokens

Unauthorized Users

Did you notice that some routes have meta: {requiresAuth: true}, attached to them? These routes shouldn't be accessible to unauthenticated users.

For example, what happens if you navigate to http://localhost:8080/profile when you're not authenticated? You should be able to view the page but no data loads, right? Let's change that so the user is redirected to the /login route instead.

So, to prevent unauthorized access, let's add a Navigation Guard to services/frontend/src/router/index.js:

import Vue from 'vue';
import VueRouter from 'vue-router';

import store from '@/store';  // NEW

import Dashboard from '@/views/Dashboard';
import EditNote from '@/views/EditNote';
import Home from '@/views/Home.vue';
import Login from '@/views/Login';
import Note from '@/views/Note';
import Profile from '@/views/Profile';
import Register from '@/views/Register';

Vue.use(VueRouter);

const routes = [
  ...
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes,
});

// NEW
router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (store.getters.isAuthenticated) {
      next();
      return;
    }
    next('/login');
  } else {
    next();
  }
});

export default router;

Log out. Then, test http://localhost:8080/profile again. You should be redirected back to the /login route.

Expired Tokens

Remember that the token expires after thirty minutes:

ACCESS_TOKEN_EXPIRE_MINUTES = 30

When this happens, the user should be logged out and redirected to the log in page. To handle this, let's add an Axios Interceptor to services/frontend/src/main.js:

import 'bootstrap/dist/css/bootstrap.css';
import axios from 'axios';
import Vue from 'vue';

import App from './App.vue';
import router from './router';
import store from './store';


axios.defaults.withCredentials = true;
axios.defaults.baseURL = 'http://localhost:5000/';  // the FastAPI backend

Vue.config.productionTip = false;

// NEW
axios.interceptors.response.use(undefined, function (error) {
  if (error) {
    const originalRequest = error.config;
    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      store.dispatch('logOut');
      return router.push('/login')
    }
  }
});

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app');

If you'd like to test, change ACCESS_TOKEN_EXPIRE_MINUTES = 30 to something like ACCESS_TOKEN_EXPIRE_MINUTES = 1. Keep in mind that the cookie itself still lasts for 30 minutes. It's the token that expires.

Conclusion

This tutorial covered the basics of setting up a CRUD app with Vue and FastAPI. Along with the apps, you also used Docker to simplify development and added authentication.

Check your understanding by reviewing the objectives from the beginning and going through each of the challenges below.

You can find the source code in the fastapi-vue repo on GitHub. Cheers!

--

Looking for more?

  1. Test all the things. Stop hacking. Start ensuring that your applications work as expected. Need help? Check out the following resources:
  2. Add alerts to display proper success and error messages to end users. Check out the Alert Component section from Developing a Single Page App with Flask and Vue.js for more on setting up alerts with Bootstrap and Vue.
  3. Add a new endpoint to the backend that gets called when a user logs out that updates the cookie like so.

Original article source at: https://testdriven.io/

#fastapi #vue 

How to Developing A Single Page App with FastAPI and Vue.js
Nat  Grady

Nat Grady

1668762720

How to Combining Flask and Vue

How can I combine Vue.js with Flask?

So you've finally got Flask under your belt, and you're no stranger to JavaScript. You've even developed a few web applications, but you start to realize something -- you have excellent functionality, but your UX is kind of bland. Where's the application flow and seamless navigation you see on many popular websites and apps today? How can that be achieved?

As you become more invested in your websites and web apps, you'll probably want to add more client-side functionality and reactivity to them. Modern web development typically achieves this through the use of front-end frameworks, and one such framework that is quickly rising in popularity is Vue (also known as Vue.js or VueJS).

Depending on your project's requirements, there are a few different ways to build a web application with Flask and Vue, and they each involve various levels of back-end/front-end separation.

In this article, we'll take a look at three different methods for combining Flask and Vue:

  1. Jinja Template: Importing Vue into a Jinja template
  2. Single-Page Application: Building a Single-Page Application (SPA) to completely separate Flask and Vue
  3. Flask Blueprint: Serving up Vue from a Flask blueprint to partially separate the two

Different Ways to Build a Web App with Flask and Vue

We'll analyze the pros and cons of each method, look at their best use cases, and detail how to set each of them up.

Jinja Template

Regardless of whether you're using React, Vue, or Angular, this is the easiest way to transition to using a front-end framework.

In many cases, when you're building a front-end for your web app, you design it around the front-end framework itself. With this method, however, the focus is still on your back-end Flask application. You'll still use Jinja and server-side templating along with a bit of reactive functionality with Vue if and when you need it.

You can import the Vue library either through a Content Delivery Network (CDN) or by serving it yourself along with your app, while setting up and routing Flask as you would normally.

Pros

  • You can build your app your way instead of fitting it around Vue's foundation.
  • Search Engine Optimization (SEO) doesn't require any additional configuring.
  • You can take advantage of cookie-based authentication instead of token-based authentication. This tends to be easier, as you're not dealing with asynchronous communication between the front and back end.

Cons

  • You have to import Vue on and set up each page individually, which can be difficult if you start adding Vue to more and more pages. It may require a number of workarounds as well since it's not really the intended way to use either Flask or Vue.

Best For

  • Small web apps literally using a single HTML page or two (as opposed to a SPA with its own dynamic routing -- see the SPA method for more info).
  • Building functionality into an already existing web app.
  • Adding bits of reactivity to an app without fully committing to a front-end framework.
  • Web apps that don't need to communicate as frequently to a back-end via AJAX.

Additional Dependencies

This method just requires the Vue library, which you can add via a CDN:

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

Setup

Out of all the methods, this setup is the simplest.

Create a folder to hold all of your app's code. Inside of that folder, create an app.py file as you would normally:

from flask import Flask, render_template # These are all we need for our purposes

app = Flask(__name__)

@app.route("/")
def index():
    return render_template("index.html", **{"greeting": "Hello from Flask!"})

We only need to import Flask and render_template from flask.

The greeting variable will come up again in a second when we look at how to render variables with both Jinja and Vue in the same file.

Next, create a "templates" folder to hold our HTML file. Inside of that folder, create an index.html file. In the body of our HTML file, create a container div with an id of vm.

It's worth noting that vm is just a common naming standard. It stands for ViewModel. You can name it whatever you want; it does not need to be vm.

Within the div, create two p tags to serve as placeholders for our Flask and Vue variables:

  1. One of the divs should contain the word 'greeting' surrounded by braces: {{ greeting }}.
  2. The other should contain 'greeting' surrounded by brackets: [[ greeting ]].

If you don't use separate delimiters, with the default setup, Flask will replace both greetings with whatever variable you pass with it (i.e., "Hello from Flask!").

Here's what we have thus far:

<body>
<!-- The id 'vm' is just for consistency - it can be anything you want -->
    <div id="vm">
        <p>{{ greeting }}</p>
        <p>[[ greeting ]]</p>
    </div>
</body>

Before the end of the body tag, import Vue from the official CDN along with a script to hold our JavaScript code:

<body>
<!-- The id 'vm' is just for consistency - it can be anything you want -->
    <div id="vm">
        <p>{{ greeting }}</p>
        <p>[[ greeting ]]</p>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="{{ url_for('static', filename='index.js') }}"></script>
</body>

Navigating up a directory, create a "static" folder. Add a new JavaScript file in that folder called index.js.

In this file, create the Vue context, set our instance's el as '#vm', change the default delimiters from '{{', '}}' to '[[', ']]':

const vm = new Vue({ // Again, vm is our Vue instance's name for consistency.
    el: '#vm',
    delimiters: ['[[', ']]']
})

In reality, we can use anything we want as our delimiters. In fact, if it's your preference, you can change the delimiters for your Jinja templates in Flask instead.

Finally, add a data element with the key/value of greeting: 'Hello, Vue!':

const vm = new Vue({ // Again, vm is our Vue instance's name for consistency.
    el: '#vm',
    delimiters: ['[[', ']]'],
    data: {
        greeting: 'Hello, Vue!'
    }
})

And now we're done with that file. Your final folder structure should look something like:

├───app.py
├───static
│   └───index.js
└───templates
    └───index.html

Now you can go back to you root project folder and run the app with flask run. Navigate to the site in your browser. The first and second line should have been replaced by Flask and Vue, respectively:

Hello from Flask!
Hello, Vue!

That's it! You can mix and match JSON endpoints and HTML endpoints as you please, but be aware that this can get really ugly really quickly. For a more manageable alternative, see the Flask Blueprint method.

With each additional HTML page, you'll have to either import the same JavaScript file and account for variables and elements that may not apply to it or create a new Vue object for each page. A true SPA will be difficult, but not impossible -- theoretically you could write a tiny JavaScript library that will asynchronously grab HTML pages/elements served by Flask.

I've actually created my own JavaScript library for this before. It was a big hassle and honestly not worth it, especially considering JavaScript will not run script tags imported this way, unless you build the functionality yourself. You'll also be reinventing the wheel.

If you'd like to check out my implementation of this method, you can find it on GitHub. The library takes a given chunk of HTML and replaces the specified HTML on the page with it. If the given HTML contains no <script> elements (it checks using regex), it simply uses HTMLElement.innerHTML to replace it. If it does contain <script> elements, it recursively adds the nodes, recreating any <script> nodes that come up, allowing your JavaScript to run.

Using something like this in combination with the History API can help you build a small SPA with a very tiny file size. You can even create your own Server-Side Rendering (SSR) functionality by serving full HTML pages on page load and then serve up partial pages through AJAX requests. You can learn more about SSR in the SPA with Nuxt method.

Single-Page Application

If you want to build a fully dynamic web app with a seamless User Experience (UX), you can completely separate your Flask back-end from your Vue front-end. This may take learning a whole new way of thinking when it comes to web app design if you're not familiar with modern front-end frameworks.

Developing your app as a SPA may put a dent in your SEO. In the past, this hit would be much more dramatic, but updates to how Googlebot indexes sites have negated this at least somewhat. It may, however, still have a greater impact on non-Google search engines that don't render JavaScript or those that snapshot your page(s) too early -- the latter shouldn't happen if your website is well-optimized.

For more information on SEO in modern SPAs, this article on Medium shows how Googlebot indexes JavaScript-rendered sites. Additionally, this article talks in-depth about the same thing along with other helpful tips concerning SEO on other search engines.

With this method, you'll want to generate a completely separate Vue app using the Vue CLI tool. Flask will then be used to serve up a JSON RESTful API that your Vue SPA will communicate with via AJAX.

Pros

  • Your front- and back-end will be completely independent of each other, so you can make changes to one without it impacting the other.
    • This allows them to be deployed, developed, and maintained separately.
    • You can even set up a number of other front-ends to interact with your Flask API if you'd like.
  • Your front-end experience will be much smoother and more seamless.

Cons

  • There is much more to set up and learn.
  • Deployment is difficult.
  • SEO might suffer without further intervention (see the SPA with Nuxt method for more details).
  • Authentication is much more involved, as you'll have to keep passing your auth token (JWT or Paseto) to your back-end.

Best For

  • Apps where UX is more important than SEO.
  • Back-ends that need to be accessible by multiple front-ends.

Additional Dependencies

  • Node/npm
  • Vue CLI
  • Flask-CORS

Deployment and containerization are outside of the scope of this article, but it's not terribly difficult to Dockerize this setup to simplify deployment.

Setup

Because we're completely separating Vue from Flask, this method requires a bit more setup. We'll need to enable Cross-Origin Resource Sharing (CORS) in Flask, since our front- and back-end will be served on separate ports. To accomplish this quickly and easily, we'll use the Flask-CORS Python package.

For security reasons, modern web browsers do not allow client-side JavaScript to access resources (such as JSON data) from an origin differing from the origin your script is on unless they include a specific response header letting the browser know it's okay.

If you haven't yet installed Flask-CORS, do so with pip.

Let's start with our Flask API.

First, create a folder to hold the code for your project. Inside, create a folder called "api". Create an app.py file in the folder. Open the file with your favorite text editor. This time we'll need to import Flask from flask and CORS from flask_cors. Because we're using flask_cors to enable cross-origin resource sharing, wrap the app object (without setting a new variable) with CORS: CORS(app). That's all we have to do to enable CORS on all of our routes for any origin.

Although this is fine for demonstration purposes, you probably aren't going to want just any app or website to be able to access your API. In that case, you can use the kwarg 'origins' with the CORS function to add a list of acceptable origins -- i.e., CORS(app, origins=["origin1", "origin2"])

For more information on Cross-Origin Resource Sharing, MDN has some great documentation on it.

Lastly, create a single greeting route at /greeting to return a JSON object with a single key/value:

{"greeting": "Hello from Flask!"}

Here's what you should have ended up with:

from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
CORS(app)

@app.route("/greeting")
def greeting():
    return {"greeting": "Hello from Flask!"}

That's all we need to do with Python.

Next, we'll set up our Vue webapp. From a terminal, open your project's root folder. Using the Vue CLI, create a Vue project called "webapp" (vue create webapp). You can use pretty much whatever options you like, but if you're using class-based components in TypeScript, the syntax will look a bit different.

When your project is finished being created, open App.vue.

Since our goal is just to see how Vue and Flask interact with each other, at the top of the page, delete all elements within the div with the id of app. You should just be left with:

<template>
<div id="app">
</div>
</template>

Within #app, create two p elements:

  1. The content of the first should be {{ greeting }}.
  2. The content of the second should be {{ flaskGreeting }}.

Your final HTML should be as such:

<template>
<div id="app">
    <p>{{ greeting }}</p>
    <p>{{ flaskGreeting }}</p>
</div>
</template>

In our script, let's add logic to show a purely client-side greeting (greeting) and a greeting pulled from our API (flaskGreeting).

Within the Vue object (it begins with export default), create a data key. Make it a function that returns an object. Then, within this object, create two more keys: greeting and flaskGreeting. greeting's value should be 'Hello, Vue!' while flaskGreeting's should be an empty string.

Here's what we have thus far:

export default {
    name: 'App',
    components: {
        HelloWorld
    },
    data: function(){
        return {
            greeting: 'Hello, Vue!',
            flaskGreeting: ''
        }
    }
}

Finally, let's give our Vue object a created lifecycle hook. This hook will only be run once the DOM is loaded and our Vue object is created. This allows us to use the fetch API and interact with Vue without anything clashing:

export default {
    components: {
        Logo
    },
    data: function(){
        return {
            greeting: 'Hello, Vue!',
            flaskGreeting: ''
        }
    },
    created: async function(){
        const gResponse = await fetch("http://localhost:5000/greeting");
        const gObject = await gResponse.json();
        this.flaskGreeting = gObject.greeting;
    }
}

Looking at the code, we're awaiting a response to our API's 'greeting' endpoint (http://localhost:5000/greeting), awaiting that response's asynchronous .json() response, and setting our Vue object's flaskGreeting variable to the returned JSON object's value for its greeting key.

For those unfamiliar with JavaScript's relatively new Fetch API, it's basically a native AXIOS killer (at least as far as the client-side is concerned -- it's not supported by Node, but it will be by Deno). Additionally, if you like consistency, you can also check out the isomorphic-fetch package in order to use Fetch on the server-side.

And we're finished. Now because, again, our front- and back-end are separate, we'll need to run both of our apps separately.

Let's open our project's root folder in two separate terminal windows.

In the first, change into the "api" directory, and then run flask run. If all goes well, the Flask API should be running. In the second terminal, change into the "webapp" directory and run npm run serve.

Once the Vue app is up, you should be able to access it from localhost:8080. If everything works, you should be greeted twice -- once by Vue, and again from your Flask API:

Hello, Vue!
Hello from Flask!

Your final file tree should look like:

├───app.py
├───api
│   └───app.py
└───webapp
    ... {{ Vue project }}

Single-Page Application with Nuxt

If SEO is as important to you as UX, you might want to implement Server-Side Rendering (SSR) in some format.

SSR makes it easier for search engines to navigate and index your Vue app, as you'll be able to give them a form of your app that doesn't require JavaScript to generate. It can also make it quicker for users to interact with your app, since much of your initial content will be rendered before it's sent to them. In other words, users will not have to wait for all of your content to load asynchronously.

A Single-Page App with Server-Side Rendering is also called a Universal App.

Although it's possible to implement SSR manually, we'll use Nuxt in this article; it greatly simplifies things.

Just like with the SPA method, your front- and back-end will be completely separate; you'll just be using Nuxt instead of the Vue CLI.

Pros

  • All of the pros of the SPA method with the addition of Server-Side Rendering.

Cons

  • About as difficult to set up as the SPA method.
  • Conceptually, there's even more to learn as Nuxt is essentially just another layer on top of Vue.

Best For

  • Apps where SEO is as important as UX.

Additional Dependencies

  1. Node/npm
  2. Nuxt
  3. Flask-CORS

Setup

This is going to be very similar to the SPA method. In fact, the Flask portion is the exact same. Follow along with it until you have created your Flask API.

Once your API is finished, within your terminal, open your project's root folder and run the command npx create-nuxt-app webapp. This will let you interactively generate a new Nuxt project without installing any global dependencies.

Any options should be fine here.

Once your project is done being generated, dive into your new "webapp" folder. Within the "pages" folder, open index.vue in your text editor. Similarly, delete everything within the div that has the class container. Inside the div, create two p tags with the same vars: {{ greeting }} and {{ flaskGreeting }}.

It should look like this:

<template>
<div class="container">
    <p>{{ greeting }}</p>
    <p>{{ flaskGreeting }}</p>
</div>
</template>

And now for our script:

  • Add a data key that returns an object with the variables greeting and flaskGreeting
  • Add a created lifecycle hook:
    • await fetch to get the JSON greeting from our API (on port 5000 unless you changed it)
    • await the json() method to asynchronously get our JSON data from our API's response
    • Set our Vue instance's flaskGreeting to the greeting key from our response's JSON object

The Vue object should look like:

export default {
    components: {
        Logo
    },
    data: function(){
        return {
            greeting: 'Hello, Vue!',
            flaskGreeting: ''
        }
    },
    created: async function(){
        const gResponse = await fetch("http://localhost:5000/greeting");
        const gObject = await gResponse.json();
        this.flaskGreeting = gObject.greeting;
    }
}

Running the Nuxt/Vue app and Flask API will look very similar to the the SPA method as well.

Open two terminal windows. Within the first, change into "api" and run the flask run command. Within the second, change into "webapp" and run npm run dev to start a development server for your Nuxt project.

Once the Nuxt app is up, you should be able to access it from localhost:3000:

Hello, Vue!
Hello from Flask!

In production you can run npm run build and then npm run start to start a production server.

Our final tree:

├───app.py
├───api
│   └───app.py
└───webapp
    ... {{ Nuxt project }}

BONUS: Vue vs Nuxt SEO Comparison

I mentioned the benefits of SEO earlier in this article, but just to show you what I meant, I ran both web apps as-is and grabbed the Lighthouse SEO scores for both.

With no changes to either app, here's what we have:

Lighthouse SEO Scores for our Vue and Nuxt App

Again, there are things you can do to improve your pure Vue SEO score. Lighthouse in Chrome's dev tools mentions adding a meta description, but with no additional intervention, Nuxt gave us a perfect SEO score.

Additionally, you can actually see the difference between the SSR that Nuxt does and vanilla Vue's completely asynchronous approach. If you run both apps at the same time, navigate to their respective origins, localhost:8080 and localhost:3000, the Vue app's initial greeting happens milliseconds after you get the response, whereas Nuxt's is served with its initial greeting already-rendered.

For more information on the differences between Nuxt and Vue, you can check out these articles:

  1. Nuxt.js over Vue.js: when should you use it and why
  2. How Nuxt.js solves the SEO problems in Vue.js.

Flask Blueprint

Perhaps you already have a small Flask app developed and you want to build a Vue app as more of a means to an end rather than as the main event.

Examples:

  1. Prototype to demonstrate functionality to your employer or client (you can always replace this or hand it off to a front-end developer later on).
  2. You just don't want to deal with the potential frustration that could result when deploying completely separate front- and back-end.

In that case you could sort-of meet in the middle by keeping your Flask app, but building on a Vue front-end within its own Flask blueprint.

This will look a lot like the Jinja Template method, but the code will be more organized.

Pros

  • No need to build a complex front-end if it isn't necessary.
  • Similar to the Jinja Template method with the added benefit of better code organization.
  • You can always expand the front- and back-end as needed later on.

Cons

  • Workarounds might be necessary to allow a full SPA.
  • Accessing the API might be slightly more annoying from a separate front-end (such as a mobile app) as the front- and back-end are not completely separate.

Best For

  • Projects where functionality is more important than UI.
  • You're building a front-end onto an already-existing Flask app.
  • Small web apps that are made up of only a couple of HTML pages.

Additional Dependencies

Similarly to the Jinja Template method, we will be using a CDN to pull in the Vue library:

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

Setup

Like the other methods, create a new folder to house your code. Inside of it, create two folders: "api" and "client". Intuitively, these will contain the blueprints for our API and client (Vue), respectively.

Let's dive into the "api" folder.

Create a file called api.py. This will contain all of the code associated with our API. Additionally, because we will be accessing this file/folder as a module, create an __init__.py file:

from flask import Blueprint

api_bp = Blueprint('api_bp', __name__) # "API Blueprint"

@api_bp.route("/greeting") # Blueprints don't use the Flask "app" context. They use their own blueprint's
def greeting():
    return {'greeting': 'Hello from Flask!'}

The first argument to Blueprint is for Flask's routing system. The second, __name__, is equivalent to a Flask app's first argument (Flask(__name__)).

And that's it with our API blueprint.

Okay. Let's dive into the "client" folder we created earlier. This one's going to be a little more involved than our API blueprint, but no more complicated than a regular Flask app.

Again, like a regular Flask app, inside this folder, create a "static" folder and a "templates" folder. Create a file called client.py and open it in your text editor.

This time, we'll pass in a few more arguments to our Blueprint so it knows where to find the correct static files and templates:

client_bp = Blueprint('client_bp', __name__, # 'Client Blueprint'
    template_folder='templates', # Required for our purposes
    static_folder='static', # Again, this is required
    static_url_path='/client/static' # Flask will be confused if you don't do this
)

Add the route as well to serve up the index.html template:

from flask import Blueprint, render_template

client_bp = Blueprint("client_bp", __name__, # 'Client Blueprint'
    template_folder="templates", # Required for our purposes
    static_folder="static", # Again, this is required
    static_url_path="/client/static" # Flask will be confused if you don't do this
)

@client_bp.route("/")
def index():
    return render_template("index.html")

Excellent. Our client blueprint is now finished. Exit the file and turn to the blueprint's "templates" folder. Create an index.html file:

<body>
<!-- The id 'vm' is just for consistency - it can be anything you want -->
    <div id="vm" class="container">
        <p>[[ greeting ]]</p>
        <p>[[ flaskGreeting ]]</p>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="{{ url_for('client_bp.static', filename='index.js') }}"></script>
</body>

Did you notice that we're using brackets instead of braces? It's because we need to change the delimiters to keep Flask from catching them first.

greeting will be rendered by Vue as soon as it's ready, while flaskGreeting will be taken from a Flask response that we'll request asynchronously.

Done. Add a new file to the "static" folder called index.js. Create a variable called apiEndpoint and set it to api_v1. This just makes everything a little more DRY if we decide to change our endpoint later on:

const apiEndpoint = '/api_v1/';

We haven't created the logic for our endpoint yet. That will come in the last step.

Next, start by making the Vue context look identical to the context in the Jinja Template method:

const apiEndpoint = '/api_v1/';

const vm = new Vue({ // Again, vm is our Vue instance's name for consistency.
    el: '#vm',
    delimiters: ['[[', ']]'],
    data: {
        greeting: 'Hello, Vue!'
    }
})

Again, we created the Vue context, set our instance's el as '#vm', changed the default delimiters from '{{', '}}' to '[[', ']]', and added a data element with the key/value of greeting: 'Hello, Vue!'.

Because we're also going to pull a greeting from our API, create a data placeholder called flaskGreeting with the value of an empty string:

const apiEndpoint = '/api_v1/';

const vm = new Vue({
    el: '#vm',
    delimiters: ['[[', ']]'],
    data: {
        greeting: 'Hello, Vue!',
        flaskGreeting: ''
    }
})

Let's give our Vue object an asynchronous created lifecycle hook:

const apiEndpoint = '/api_v1/';

const vm = new Vue({
    el: '#vm',
    delimiters: ['[[', ']]'],
    data: {
        greeting: 'Hello, Vue!',
        flaskGreeting: ''
    },
    created: async function(){
        const gResponse = await fetch(apiEndpoint + 'greeting');
        const gObject = await gResponse.json();
        this.flaskGreeting = gObject.greeting;
    }
})

Looking at the code, we're awaiting a response from our API's 'greeting' endpoint (/api_v1/greeting), awaiting that response's asynchronous .json() response, and setting our Vue object's flaskGreeting variable to the returned JSON object's value for its greeting key. It's basically a mashup between the Vue objects from methods 1 and 2.

Excellent. Only one thing left to do: Let's put everything together by adding an app.py to the project root. Within the file, import flask along with the blueprints:

from flask import Flask
from api.api import api_bp
from client.client import client_bp

Create a Flask app as you would normally and register the blueprints using app.register_blueprint():

from flask import Flask
from api.api import api_bp
from client.client import client_bp

app = Flask(__name__)
app.register_blueprint(api_bp, url_prefix='/api_v1')
app.register_blueprint(client_bp)

Final file tree:

├───app.py
├───api
│   └───__init__.py
│   └───api.py
└───client
    ├───__init__.py
    ├───static
    │   └───index.js
    └───templates
        └───index.html

And that's it! If you run your new app with flask run you should be greeted twice -- once by Vue itself and again by a response from your Flask API.

Summary

There are many, many different ways in which to build a web app using Vue and Flask. It all depends on your situation at hand.

Some questions to ask:

  1. How important is SEO?
  2. What does your development team look like? If you don't have a DevOps team, do you want to take on the added complexity of having to deploy the front- and back-end separately?
  3. Are you just rapid prototyping?

Hopefully this article steers you in the right direction, giving you an idea about how to combine your Vue and Flask applications.

You can grab the final code from the combining-flask-with-vue repo on GitHub.

Original article source at: https://testdriven.io/

#flask #vue 

How to Combining Flask and Vue
Conor  Grady

Conor Grady

1668674576

How to Use `style Scoped` in Vue

In this Vue tutorial, we learn how to use `style scoped` in Vue. Here's how you can use `style scoped` in Vue to define CSS rules that apply to only one component.

Vue 3 has a handy way to locally scope the CSS in your components. Using <style scoped>, you don't need to have a single large CSS file or multiple CSS files to make your site look pretty. By simply putting the CSS in the <style scoped> tag, the CSS will apply to that component.

App.vue

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <HelloWorld msg="Welcome to Your Vue.js App" />
</template>

<script>
  import HelloWorld from "./components/HelloWorld.vue";

  export default {
    name: "App",
    components: {
      HelloWorld,
    },
  };
</script>

<style>
  #app {
    font-family: Avenir, Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    color: #2c3e50;
    margin-top: 60px;
  }
</style>

HelloWorld.vue

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <p class="text">This text is in a component with a {{ html }}</p>
  </div>
</template>

<script>
  export default {
    name: "HelloWorld",
    data() {
      return {
        html: `<style scoped>`,
      };
    },
    props: {
      msg: String,
    },
  };
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
  h3 {
    margin: 40px 0 0;
  }
  ul {
    list-style-type: none;
    padding: 0;
  }
  li {
    display: inline-block;
    margin: 0 10px;
  }
  a {
    color: #42b983;
  }
  .text {
    color: pink;
  }
</style>

Result


Original article source at: https://masteringjs.io

#vue 

How to Use `style Scoped` in Vue
Rupert  Beatty

Rupert Beatty

1668130560

Pika: An Open-source Colour Picker App for MacOS

Pika

Pika (pronounced pi·kuh, like picker) is an easy to use, open-source, native colour picker for macOS. Pika makes it easy to quickly find colours onscreen, in the format you need, so you can get on with being a speedy, successful designer.

Screenshots of the dark and light Pika interface

Requirements

OS

  • macOS Catalina (Version 10.15+) and newer

Development

Getting started with contributing

Make sure you have mint installed, and bootstrap the toolchain dependencies:

brew install mint
mint bootstrap

Open Pika.xcodeproj and you should be good to go. If you run into any problems, please detail them in an issue.

Contributions

Any and all contributions are welcomed. Check for open issues, look through the project roadmap, and submit a PR.

Dependencies and thanks

And a huge thank you to Stormnoid for the incredible 2D vector field visualisation on Shadertoy.

Download the latest version of the app at superhighfives.com/pika.

Learn more about the motivations behind the project, and the product vision.

Download Details:

Author: Superhighfives
Source Code: https://github.com/superhighfives/pika 
License: MIT license

#swift #macos #app #xcode #swiftui 

Pika: An Open-source Colour Picker App for MacOS
Rupert  Beatty

Rupert Beatty

1668126720

Merchantkit: A Modern in-App Purchases Management Framework for iOS

MerchantKit

A modern In-App Purchases management framework for iOS developers.

MerchantKit dramatically simplifies the work indie developers have to do in order to add premium monetizable components to their applications. Track purchased products, offer auto-renewing subscriptions, restore transactions, and much more.

Designed for apps that have a finite set of purchasable products, MerchantKit is a great way to add an unlockable 'pro tier' to an application, as a one-time purchase or ongoing subscription.

Example Snippets

Find out if a product has been purchased:

let product = merchant.product(withIdentifier: "iap.productidentifier")
print("isPurchased: \(merchant.state(for: product).isPurchased))"

Buy a product:

let task = merchant.commitPurchaseTask(for: purchase)
task.onCompletion = { result in 
    switch result {
        case .succeeded(_):
            print("purchase completed")
        case .failed(let error):
            print("\(error)")
    }
}

task.start()

Get notified when a subscription expires:

public func merchant(_ merchant: Merchant, didChangeStatesFor products: Set<Product>) {
    if let subscriptionProduct = products.first(where: { $0.identifier == "subscription.protier" }) {
        let state = merchant.state(for: subscriptionProduct)
        
        switch state {
            case .isPurchased(let info):
                print("subscribed, expires \(info.expiryDate)")
            default:
                print("does not have active subscription")
        }
    }
}

Project Goals

  • Straightforward, concise, API to support non-consumable, consumable and subscription In-App Purchases.
  • Simplify the development of In-App Purchase interfaces in apps, including localized formatters to dynamically create strings like "£2.99 per month" or "Seven Day Free Trial".
  • No external dependencies beyond what Apple ships with iOS. The project links Foundation, StoreKit, SystemConfiguration and os for logging purposes.
  • Prioritise developer convenience and accessibility over security. MerchantKit users accept that some level of piracy is inevitable and not worth chasing.
  • Permissive open source license.
  • Compatibility with latest Swift version using idiomatic language constructs.

The codebase is in flux right now and the project does not guarantee API stability. MerchantKit is useful, it works, and will probably save you time. That being said, MerchantKit is by no means finished. The test suite is patchy.

Installation

CocoaPods

To integrate MerchantKit into your Xcode project using CocoaPods, specify it in your Podfile.

pod 'MerchantKit'

Carthage

To integrate MerchantKit into your Xcode project using Carthage, specify it in your Cartfile.

github "benjaminmayo/merchantkit"

Manually

Compile the MerchantKit framework and embed it in your application. You can download the source code from Github and embed the Xcode project into your app, although you'll have to upgrade to the latest releases manually.

Getting Started

In your app delegate, import MerchantKit create a Merchant instance in application(_:, didFinishLaunchingWithOptions:). Supply a configuration (such as Merchant.Configuration.default) and a delegate.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    ...
    
    self.merchant = Merchant(configuration: .default, delegate: self)
    
    ...
}

Register products as soon as possible (typically within application(_:, didFinishLaunchingWithOptions:)). You may want to load Product structures from a file, or simply declaring them as constants in code. These constants can then be referred to statically later.

let product = Product(identifier: "iap.productIdentifier", kind: .nonConsumable)
let otherProduct = Product(identifier: "iap.otherProductIdentifier", kind: .subscription(automaticallyRenews: true)) 
self.merchant.register([product, otherProduct])

Call setup() on the merchant instance before escaping the application(_:, didFinishLaunchingWithOptions:) method. This tells the merchant to start observing the payment queue.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    ...
    
    self.merchant = Merchant(configuration: .default, delegate: self)    
    self.merchant.register(...)
    self.merchant.setup()
    
    ...
}

Profit! Or something.

Merchant Configuration

Merchant is initialized with a configuration object; an instance of Merchant.Configuration. The configuration controls how Merchant validates receipts and persist product state to storage. Most applications can simply use Merchant.Configuration.default and get the result they expect. You can supply your own Merchant.Configuration if you want to do something more customized.

Tip: MerchantKit provides Merchant.Configuration.usefulForTestingAsPurchasedStateResetsOnApplicationLaunch as a built-in configuration. This can be used to test purchase flows during development as the configuration does not persist purchase states to permanent storage. You can repeatedly test 'buying' any Product, including non-consumables, simply by restarting the app. As indicated by its unwieldy name, you should not use this configuration in a released application.

Merchant Delegate

The delegate implements the MerchantDelegate protocol. This delegate provides an opportunity to respond to events at an app-wide level. The MerchantDelegate protocol declares a handful of methods, but only one is required to be implemented.

func merchant(_ merchant: Merchant, didChangeStatesFor products: Set<Product>) {
    // Called when the purchased state of a `Product` changes.
    
    for product in products {
        print("updated \(product)")
    }
}

The delegate optionally receives loading state change events, and a customization point for handling Promoted In-App Purchase flows that were initiated externally by the App Store. Sensible default implementations are provided for these two methods.

Product Interface Controller

The tasks vended by a Merchant give developers access to the core operations to fetch and purchase products with interfaces that reflect Swift idioms better than the current StoreKit offerings. Many apps will not need to directly instantiate tasks. ProductInterfaceController is the higher-level API offered by MerchantKit that covers the use case of many projects. In an iOS app, a view controller displaying an upgrade screen would be backed by a single ProductInterfaceController which encapsulated all necessary product and purchasing logic.

The ProductInterfaceController class encompasses common behaviours needed to present In-App Purchase for sale. However, it remains abstract enough not be tied down to one specific user interface appearance or layout.

Developers simply provide the list of products to display and tells the controller to fetch data. The delegate notifies the app when to update its custom UI. It handles loading data, intermittent network connectivity and in-flight changes to the availability and state of products.

See the Example project for a basic implementation of the ProductInterfaceController.

Formatters

MerchantKit includes several formatters to help developers display the cost of In-App Purchases to users.

PriceFormatter is the simplest. Just give it a Price and it returns formatted strings like '£3.99' or '$5.99' in accordance with the store's regional locale. You can specify a custom string if the price is free. SubscriptionPriceFormatter takes a Price and a SubscriptionDuration. Relevant Purchase objects exposes these values so you can simply pass them along the formatter. It generates strings like '$0.99 per month', '£9.99 every 2 weeks' and '$4.99 for 1 month' depending on the period and whether the subscription will automatically renew.

In addition to the renewal duration, subscriptions can include free trials and other introductory offers. You can use a SubscriptionPeriodFormatter to format a text label in your application. If you change the free trial offer in iTunes Connect, the label will dynamically update to reflect the changed terms without requiring a new App Store binary. For example:

func subscriptionDetailsForDisplay() -> String? {
    guard let terms = purchase.subscriptionTerms, let introductoryOffer = terms.introductoryOffer else { return nil }
    
    let formatter = SubscriptionPeriodFormatter()
    
    switch introductoryOffer {
        case .freeTrial(let period): return "\(formatter.string(from: period)) Free Trial" // something like '7 Day Free Trial'
        default: ...
    }
}

PriceFormatter works in every locale supported by the App Store. SubscriptionPriceFormatter and SubscriptionPeriodFormatter are currently offered in a small subset of languages. Voluntary translations are welcomed.

See the Example project for a demo where you can experiment with various configuration options for PriceFormatter and SubscriptionPriceFormatter.

Consumable Products

Merchant tracks the purchased state of non-consumable and subscription products. Consumable products are considered transitory purchases and not recorded beyond the initial time of purchase. Because of their special nature, they must be handled differently. Ensure you supply a consumableHandler when creating the Merchant. This can by any object that conforms to the MerchantConsumableProductHandler protocol. The protocol has a single required method:

func merchant(_ merchant: Merchant, consume product: Product, completion: @escaping () -> Void) {
    self.addCreditsToUserAccount(for: product, completion: completion) // application-specific handling 
}

The Merchant will always report a consumable product's state as PurchasedState.notPurchased. Forgetting to implement the delegate method will result in a runtime fatal error.

To Be Completed (in no particular order)

  • Increase the number of localizations for SubscriptionPriceFormatter and SubscriptionPeriodFormatter.
  • Add extensive documentation with more examples in the Example project.
  • Support downloadable content In-App Purchases.
  • Probably a lot more stuff I haven't thought of yet.

Credits

Developed and managed by Benjamin Mayo, @bzamayo on Twitter.

Download Details:

Author: Benjaminmayo
Source Code: https://github.com/benjaminmayo/merchantkit 
License: MIT license

#swift #ios #app #purchases 

Merchantkit: A Modern in-App Purchases Management Framework for iOS
Rupert  Beatty

Rupert Beatty

1667921160

Latest: A Small Utility App for The Mac

Latest 

This is Latest, a small utility app for the Mac. Latest is a free and open-source app for macOS that checks if all your apps are up to date. Get a quick overview of which apps changed and what changed and update them right away. Latest currently supports apps downloaded from the Mac App Store and apps that use Sparkle for updates, which covers most of the apps on the market.

Latest is developed in my free time, so occasional updates may happen. Take a look at the Issues section to see what's coming. If you have an idea for a new feature, or encounter any bugs, feel free to open a new issue. I am thankful for contributions. Check out the section below for more information.

Latest

Installation

There are multiple ways to install the app.

Download the App

The easiest way to install Latest is to download the latest release as an app. You unzip the download by double-clicking on it (if that does not happen automatically) and then move the Latest.app into the Applications folder.

If you would like to check out earlier versions, head over to the Releases page to browse the history of Latest.

Homebrew Cask

Latest can also be installed via Homebrew Cask. If you have not installed Homebrew, follow the simple instructions here. After that, run brew install --cask latest to install the current version of Latest.

Build from Source

To build Latest, Xcode 11 and Swift 5 is required.

You can build Latest directly on your machine. To do that, you have to download the source code by cloning the repository: git clone --recurse-submodules git@github.com:mangerlahn/Latest.git.

Then you can open the Latest.xcodeproj and hit Build and Run. Make sure that the Latest scheme is selected.

Contribution

I am thankful for all contributions to the project. You can contribute typo-fixes, translations, code and of course suggestions, wishes, and bug reports.

Translations

The text used in Latest is hosted by the kind people over at Weblate. If you would like to add a new language, or improve an existing one, here is your starting point.

Translation status

Code

Take a look at the Issues section to see what you can do. If you have your own idea, and it does not appear in the issues list, please add it first. I don't think that I would reject any pull request, but it is useful to know about your idea earlier. Imagine two people have the same idea at the same time and both put a lot of work into that just to find out that someone else has made the same when it's too late.

I would like to assign every issue to the person working on that particular thing, so if you would like to implement something, leave a small note in the issue. I will assign the issue to you and it's yours.

Donation

As mentioned above, Latest is free for you to use. I work on the app in my spare time. If you would like to support the development by donating, you can do so here.

Download Details:

Author: Mangerlahn
Source Code: https://github.com/mangerlahn/Latest 
License: GPL-3.0 license

#swift #app #macos 

Latest: A Small Utility App for The Mac
Rupert  Beatty

Rupert Beatty

1667842620

Attabench: Microbenchmarking App for Swift with Nice Log-log Plots

Attabench

Attabench is a microbenchmarking app for macOS, designed to measure and visualize the performance of Swift code.

Screenshot of Attabench app

Background

This app is for microbenchmarking low-level algorithms with one degree of freedom (usually size). It works by repeatedly performing the same operation on random data of various sizes, while continuously charting the results in nice plots. Attabench's default log-log plots are ideal for seeing your algorithm's performance at a glance.

Attabench was originally created to supply nice log-log charts for my dotSwift 2017 talk and Optimizing Collections book. At the time, it seemed easier to build a custom chart renderer from scratch using Core Graphics than to mess with a bunch of CSV files and pivot tables in Excel. (It has to be noted though that this opinion has been somewhat weakened during the implementation process.)

Attabench was made in a hurry for a single use case, so its code is what polite people might call a little messy. But it's shockingly fun to play with, and the graphs it produces are chock full of strange and wonderful little mysteries.

If you find Attabench useful in your own project, please consider buying a copy of my book! It contains a lot of benchmarks made with Attabench; I'm positive you'll find it entertaining and informative.

Installation

Follow these steps to compile Attabench on your own:

Clone this repo to your Mac.

git clone https://github.com/attaswift/Attabench.git Attabench --recursive
cd Attabench

Install Carthage if you don't already have it. (This assumes you have Homebrew installed.)

brew install carthage

Retrieve and build dependencies (SipHash, BTree and GlueKit).

carthage bootstrap --platform Mac

Open the project file in Xcode 9, then build and run the Attabench target.

open Attabench.xcodeproj

Usage

Attabench has two document formats: a benchmark document defining what to test (with the extension .attabench), and a results document containing benchmark timings (extension .attaresult). Results documents remember which benchmark they came from, so you can stop Attabench and restart any particular benchmark run at any point. You may create many result documents for any benchmark.

When the app starts up, it prompts you to open a benchmark document. Each benchmark contains executable code for one or more individually measurable tasks that can take some variable input. The repository contains two examples, so you don't need to start from sratch:

SampleBenchmark.attabench is a simple example benchmark with just three tasks. It is a useful starting point for starting your own benchmarks.

OptimizingCollections.attabench is an example of a real-life benchmark definition. It was used to generate the charts in the Optimizing Collections book. (See if you can reproduce my results!)

We are going to look at how to define your own benchmark files later; let's just play with the app first.

Once you load a benchmark, you can press ⌘-R to start running benchmarks with the parameters displayed in the toolbar and the left panel. The chart gets updated in real time as new measurements are made.

Screenshot of Attabench app

You can follow the benchmarking progress by looking at the status bar in the middle panel. Below it there is a console area that includes Attabench status messages. If the benchmark prints anything on standard output or standard error during its run, that too will get included in the console area.

Control What Gets Measured

You can use the checkboxes in the list inside the left panel to control which tasks get executed. If you have many tasks, you can filter them by name using the search field on the bottom. (You can build simple expressions using negation, conjunction (AND) and disjunction (OR) -- for example, typing dog !brown, cat in the search field will get you all tasks whose name includes either dog but not brown, or it includes cat.) To check/uncheck many tasks at once, just select them all and press any of their checkboxes.

The two pop up buttons in the tool bar lets you select the size interval on which you want to run your tasks. Attabench will smoothly sample the interval on a logarithmic curve that perfectly fits the charts.

While there is an active benchmark running, whenever you change something on the left side of the window, the benchmark is immediately stopped and restarted with the new parameters. This includes typing anything in the search bar -- only visible tasks get run. Be careful not to interrupt long-running measurements.

The run options panel in the bottom controls how many times any particular task is executed before a measurement is reported. As a general rule, the task is repeated Iterations times, and the fastest result is used as the measurement. However, when the Min Duration field is set, each task will keep repeating until the specified time has elapsed; this smooths out the charts of super quick benchmarks that would otherwise have really noisy results. On the other hand, a task will not get repeated when it has cumulatively taken more time than the time interval in Max Duration field. (This will get you results quicker for long-running tasks.) So with the Duration fields, any particular task may get run for either more or less times than what you set in the Iteration field.

Change How Data Gets Displayed

The right panel is used to configure how the chart is rendered. Feel free to experiment by tweaking these controls; they only change the appearance of how the results get displayed; they do not affect any currently running benchmark.

The pop up button on the top lets select one from a handful of built-in visual themes to radically change the chart's appearance. For example, the Presentation theme is nice for white-on-black presentation decks. (You currently need to modify the source of the app if you need to change these themes or create your own.)

The three Scales checkboxes lets you enable/disable amortized time display or switch to linear scales on any of the two axes. These are occasionally useful.

The Curves panel includes two pop up buttons for selecting what data to display. For the actual curves, you can choose from the following kinds of data:

  • None to disable the curve altogether
  • Minimum to show the minimum of all collected measurements.
  • Average selects the arithmetic mean of collected samples. This is often the most informative, so this is the default choice.
  • Maximum displays the slowest measurement only. This is probably not that useful on its own, but it was really cheap to implement! (And it can be interesting to combine it with the stddev-based error bands.)
  • Sample Count is the odd one out: it displays the count of measurements made, not their value. This is a bit of a hack, but it is very useful for determining if you have taken enough measurements. (To get the best view, switch to a linear "time" scale.)

There is also an optional Error Band that you can display around each curve. Here are the available options for these bands:

  • None disables them. This is the minimalist choice.
  • Maximum paints a faintly colored band between the minimum and maximum measured values.
  • The μ + σ option replaces the maximum value with the sum of the average and the standard deviation. (This is the 68% in the 68-95-99.7 rule.)
  • μ + 2σ doubles the standard deviation from the previous option. ("95%")
  • μ + 3σ goes triple. ("99.7%")

The bottom band is always set to the minimum value, in all cases except None. (E.g., μ - σ can easily go below zero, which looks really bad on a log scale.)

A word of warning: I know nothing about statistics, and I'm not qualified to do proper statistical analysis. I chose these options because they produced cool-looking charts that seemed to tell me something meaningful about the spread of the data. These sigma expressions look suitably scientific, but they are likely not the greatest choice for benchmarking. (I'm pretty sure benchmark measurements don't follow a normal distribution.) If you do know this sort of thing, please submit a PR to fix things!

The Visible Ranges panel lets you select what ranges of values to display on the chart. By default, the chart is automatically scaled to fit all existing measurements for the active tasks and the entire active range. Setting specific ranges is useful if you need to zoom into a part of the chart; sorry you can't do this directly on the chart view.

Finally, the two Performance fields lets you control how often Attabench updates the UI status and how often it redraws the chart. If results come too quickly, the CPU spent on Attabench's UI updates could easily affect measurements.

Get Chart Images Out of Attabench

To get a PNG version of the current chart, simply use the mouse to drag the chart into Finder or another app. Attabench also includes a command-line tool to automate the rendering of charts -- check out the attachart executable target in the Swift package. You can use it to prevent wrist fatigue when you need to generate more than a handful of images. (Saving the command line invocations into a script will also let you regenerate the whole batch later.)

Create Your Own Benchmarks

90% the fun of Attabench is in defining and running your own benchmarks. The easiest way to do that is to make a copy of the included SampleBenchmark.attabench benchmark and then modify the code in it.

Anatomy of an Attabench Benchmark Document

An .attabench document is actually a folder containing the files needed to run the benchmark tasks. The only required file is run.sh; it gets executed every time Attabench needs to run a new measurement. The one in SampleBenchmark uses the Swift Package Manager to build and run the Swift package that's included in the folder. (You can define benchmarks in other languages, too; however, you'll need to implement the Attabench IPC protocol on your own. Attabench only provides a client implementation in Swift.)

Defining Tasks in Swift

SampleBenchmark contains a Swift package that is already set up to run benchmarks in Attabench; you only need to replace the example tasks with your own ones.

(To help you debug things, you may want to build the package in Terminal rather than inside Attabench. It is a normal Swift package, so you can build it and run it on its own. It even contains a set of command line options that you can use to run benchmarks directly from the command line -- this is extremely useful when you need to debug something about a task.)

To help you get started, let's describe the tasks that SampleBenchmark gives you by default.

To define a new benchmark, you need to create a new instance of the Benchmark<Input> generic class and add some tasks to it.

public class Benchmark<Input>: BenchmarkProtocol {
    public let title: String
    public var descriptiveTitle: String? = nil
    public var descriptiveAmortizedTitle: String? = nil

    public init(title: String, inputGenerator: @escaping (Int) -> Input)
    public func addTask(title: String, _ body: @escaping (Input) -> ((BenchmarkTimer) -> Void)?)    
}

Each benchmark has an Input type parameter that defines the shared input type that all tasks in that benchmark take. To create a benchmark, you also need to supply a function that takes a size (a positive integer) and returns an Input value of that size, typically using some sort of random number generator.

For example, let's create a simple benchmark that measures raw lookup performance in some standard collection types. To do that, we need to generate two things as input: a list of elements that the collection should contain, and a sequence of lookup operations to perform. We can represent both parts by randomly shuffling integers from 0 to size - 1, so that the order in which we insert elements into the collection will have no relation to the order we look them up:

let inputGenerator: (Int) -> (input: [Int], lookups: [Int]) = { size in
    return ((0 ..< size).shuffled(), (0 ..< size).shuffled())
}

Now that we have an input generator, we can start defining our benchmark:

let benchmark = Benchmark(title: "Sample", inputGenerator: inputGenerator)
benchmark.descriptiveTitle = "Time spent on all elements"
benchmark.descriptiveAmortizedTitle = "Average time spent on a single element"

We can add tasks to a benchmark by calling its addTask method. Let's start with a task that measures linear search by calling Array.contains on the input array:

benchmark.addTask(title: "Array.contains") { (input, lookups) in
    guard input.count <= 16384 else { return nil }
    return { timer in
        for value in lookups {
            guard input.contains(value) else { fatalError() }
        }
    }
}

The syntax may look strange at first, because we're returning a closure from within a closure, with the returned closure doing the actual measurement. This looks complicated, but it allows for extra functionality that's often important. In this case, we expect that the simple linear search implemented by Array.contains will be kind of slow, so to keep measurements fast, we limit the size of the input to about 16 thousand elements. Returning nil means that the task does not want to run on a particular input value, so its curve will have a gap on the chart corresponding to that particular size.

The inner closure receives a timer parameter that can be used to narrow the measurement to the section of the code we're actually interested in. For example, when we're measuring Set.contains, we aren't interested in the time needed to construct the set, so we need to exclude it from the measurement:

benchmark.addTask(title: "Set.contains") { (input, lookups) in
    return { timer in
        let set = Set(input)
        timer.measure {
            for i in lookups {
                guard set.contains(i) else { fatalError() }
            }
        }
    }
}

But preprocessing input data like this is actually better done in the outer closure, so that repeated runs of the task will not waste time on setting up the environment again:

benchmark.addTask(title: "Set.contains") { (input, lookups) in
    let set = Set(input)
    return { timer in
        for value in lookups {
            guard set.contains(value) else { fatalError() }
        }
    }
}

This variant will go much faster the second and subsequent time the app runs it.

To make things a little more interesting let's add a third task that measures binary search in a sorted array:

benchmark.addTask(title: "Array.binarySearch") { input, lookups in
    let data = input.sorted()
    return { timer in 
        for value in lookups {
            var i = 0
            var j = array.count
            while i < j {
                let middle = i + (j - i) / 2
                if value > array[middle] {
                    i = middle + 1
                }
                else {
                    j = middle
                }
            }
            guard i < array.count && array[i] == value else { fatalError() }
        }
    }
}

That's it! To finish things off, we just need to start the benchmark. The start() method parses command line arguments and starts running tasks based on the options it receives.

benchmark.start()

Get Surprised by Results

To run the new benchmark, just open it in Attabench, and press play. This gets us a chart like this one:

Sample benchmark results

The chart uses logarithmic scale on both axes, and displays amortized per-element execution time, where the elapsed time of each measurement is divided by its size.

We can often gain suprisingly deep insights into the behavior of our algorithms by just looking at the log-log charts generated by Attabench. For example, let's try explaining some of the more obvious features of the chart above:

The curves start high. Looking up just a few members is relatively expensive compared to looking up many of them in a loop. Evidently there is some overhead (initializing iteration state, warming up the instruction cache etc.) that is a significant contributor to execution time at small sizes, but is gradually eclipsed by our algorithmic costs as we add more elements.

After the initial warmup, the cost of looking up an element using Array.contains seems to be proportional to the size of the array. This is exactly what we expect, because linear search is supposed to be, well, linear. Still, it's nice to see this confirmed.

The chart of Set.contains has a striking sawtooth pattern. This must be a side-effect of the particular way the set resizes itself to prevent an overly full hash table. At the peak of a sawtooth, the hash table is at full capacity (75% of its allocated space), leading to relatively frequent hash collisions, which slow down lookup operations. However, these collisions mostly disappear at the next size step, when the table is grown to double its previous size. So increasing the size of a Set sometimes makes it faster. Neat!

In theory, Set.contains should be an O(1) operation, i.e., the time it takes should not depend on the size of the set. However, our benchmark indicates that's only true in practice when the set is small.

Starting at about half a million elements, contains seems to switch gears to a non-constant curve: from then onwards, lookup costs consistently increase by a tiny amount whenever we double the size of the set. I believe this is because at 500,000 elements, our benchmark's random access patterns overwhelm the translation lookaside buffer that makes our computers' virtual memory abstraction efficient. Even though the data still fits entirely in physical memory, it takes extra time to find the physical address of individual elements.

So when we have lots of data, randomly scattered memory accesses get really slow---and this can actually break the complexity analysis of our algorithms. Scandalous!

Array.binarySearch is supposed to take O(log(n)) time to complete, but this is again proven incorrect for large arrays. At half a million elements, the curve for binary search bends upward exactly like like Set.contains did. It looks like the curve's slope is roughly doubled after the bend. Doubling the slope of a line on a log-log chart squares the original function, i.e., the time complexity seems to have become O(log(n)*log(n)) instead of O(log(n)).

By simply looking at a chart, we've learned that at large scales, scattered memory access costs logarithmic time. Isn't that remarkable?

  1. Finally, Array.binarySearch has highly prominent spikes at powers-of-two sizes. This isn't some random benchmarking artifact: the spikes are in fact due to cache line aliasing, an interesting (if unfortunate) interaction between the processor's L2 cache and our binary search algorithm. The series of memory accesses performed by binary search on a large enough continuous array with a power-of-two size tends to all fall into the same L2 cache line, quickly overwhelming its associative capacity. Try changing the algorithm so that you optimize away the spikes without affecting the overall shape and position of the curve!

Internal Details: The Attabench Protocol

(In most cases, you don't need to know about the info in this section; however, you'll need to know it if you want to create benchmarks in languages other than Swift.)

Attabench runs run.sh with two parameters: the first is the constant string attabench, identifying the protocol version, and the second is a path to a named FIFO file that will serve as the report channel for the benchmark. (Benchmarking progress is not written to stdout/stderr to make sure you can still use print in your benchmarking code without worrying about the output getting interleaved with progress reports.)

The command to run is fed to run.sh via the stdin file. It consists of a single JSON-encoded BenchmarkIPC.Command value; the type definition contains some documentation describing what each command is supposed to do. Only a single command is sent to stdin, and the pipe is then immediately closed. When Attabench wants to run multiple commands, it will simply execute run.sh multiple times.

When the run command is given, Attabench expects run.sh to keep running indefinitely, constantly making new measurements, in an infinite loop over the specified sizes and tasks. Measurements are to be reported through the report FIFO, in JSON-encoded BenchmarkIPC.Report values. Each report must be written as a single line, including the terminating newline character.

When Attabench needs to stop a running benchmark, it sends SIGTERM (signal 15) to the process. The process is expected to exit withing 2 seconds; if it doesn't, then Attabench will kill it immediately with SIGKILL (signal 9). Normally you don't need to do anything to make this work -- but you should be aware that the benchmark may get terminated at any time, so be sure to install a signal handler for SIGTERM if you need to do any cleanup prior to exiting.

⚠️ WARNING
This package has been largely superseded by the Swift Collections Benchmark package. That package provides a portable benchmarking solution that works on all platforms that Swift supports, and it is being maintained by the Swift Standard Library team.

Download Details:

Author: Attaswift
Source Code: https://github.com/attaswift/Attabench 
License: MIT license

#swift #app #benchmark #macos 

Attabench: Microbenchmarking App for Swift with Nice Log-log Plots
Rupert  Beatty

Rupert Beatty

1667834700

Passforios: Pass for iOS - an iOS client compatible

Pass

Pass is an iOS client compatible with ZX2C4's Pass command line application. It is a password manager using GPG for encryption and Git for version control.

Pass for iOS is available in App Store with the name "Pass - Password Store", and both iPhone and iPad are supported.

Download on the App Store

You can also help us test beta versions through TestFlight [^1].

[^1]: For iOS 12 users, you can download the TestFlight app by first "purchasing it" on a Mac using your Apple ID, then going to the purchased section of the App Store on your iOS device and downloading it from there.

Features

  • Compatible with the Password Store command line tool.
  • View, copy, add, and edit password entries.
  • Encrypt and decrypt password entries by PGP keys.
  • Synchronize with your password Git repository.
  • User-friendly interface: search, long press to copy, copy and open link, etc.
  • Support one-time password tokens (two-factor authentication codes).
  • Autofill in Safari/Chrome and supported apps.

Screenshots

Usages

For more, please read the wiki page.

Building Pass for iOS

  1. Install Carthage, Go, SwiftLint, and SwiftFormat: brew install carthage go swiftlint swiftformat.
  2. Install dependencies via Carthage. Therefore, execute carthage bootstrap --platform iOS --use-xcframeworks in the root directory of the project.
  3. Run ./scripts/gopenpgp_build.sh to build GopenPGP.
  4. Open the pass.xcodeproj file in Xcode.
  5. Build & Run.

Download Details:

Author: mssun
Source Code: https://github.com/mssun/passforios 
License: MIT license

#swift #password #ios #app 

Passforios: Pass for iOS - an iOS client compatible
Hermann  Frami

Hermann Frami

1667739840

Serving: Kubernetes-based, Scale-to-zero, Request-driven Compute

Knative Serving

Knative Serving builds on Kubernetes to support deploying and serving of applications and functions as serverless containers. Serving is easy to get started with and scales to support advanced scenarios.

The Knative Serving project provides middleware primitives that enable:

  • Rapid deployment of serverless containers
  • Automatic scaling up and down to zero
  • Routing and network programming
  • Point-in-time snapshots of deployed code and configurations

For documentation on using Knative Serving, see the serving section of the Knative documentation site.

For documentation on the Knative Serving specification, see the docs folder of this repository.

If you are interested in contributing, see CONTRIBUTING.md and DEVELOPMENT.md.

Download Details:

Author: Knative
Source Code: https://github.com/knative/serving 
License: Apache-2.0 license

#serverless #kubernetes #app #networking 

Serving: Kubernetes-based, Scale-to-zero, Request-driven Compute
Monty  Boehm

Monty Boehm

1667711220

Sketchcachecleaner: Sketch Cache Cleaner

Sketch Cache Cleaner

Sketch Cache Cleaner is a macOS app that deletes hidden Sketch history files that can take a lot of space on your hard drive and that you would probably never use.

 

Sketch Cache Cleaner 

 


Warning

The app idea inspired by two blog posts: How Sketch took over 200GB of our MacBooks & How to recover 50 GB or even more by deleting Sketch caches files

Please, read them in case you want to know how it works.

If your workflow relies on automatic versioning by macOS (Time Machine etc.) - DO NOT USE THIS APP!

The app will remove all files in folder: /.DocumentRevisions-V100/


System Requirements

  • macOS 10.13+
  • Xcode 11.4+
  • Swift 5.2+

Authors

Idea & design: Yuriy Oparenko

Development: Sasha Prokhorenko


Tips

  • Use this app wisely.
  • Reboot your Mac after app use.

Download Details:

Author: Yo-op
Source Code: https://github.com/yo-op/sketchcachecleaner 
License: MIT license

#sketch #cache #macos #swift #app 

Sketchcachecleaner: Sketch Cache Cleaner
Monty  Boehm

Monty Boehm

1667699400

Generate Hundreds Of Sketch Shared Styles in A Matter Of Seconds

Sketch Styles Generator

Programmatically generate hundreds Shared Styles, all at once

Sketch Styles Generator is a plugin made for Sketch.

You can select any amount of layers (text, shapes, or both) and generate Shared Styles for all of them, at once. The Shared Styles are named like the layers. Take a look at the usage section to know more about how to use it.

Follow me on Twitter @lucaorio_ for updates, help and other stuff! 🎉

Looking for other plugins? Give these a try! 😎

Sketch Resize Sketch Reverse

Why this plugin?

  • Sketch doesn't allow to generate multiple shared styles at once
  • Sketch appends a Style suffix to the name of every style you try to create

Installation

Manual

Via Sketch Runner

  • Trigger Sketch Runner (cmd+')
  • Move to the Install tab
  • Search for Styles Generator, and install

Usage

  • Rename the layers you want to generate your shared styles from (you can speed up this process with RenameIt, or Find-And-Replace)
  • Select the layers (it doesn't matter if the selection includes both shapes, and text fields)
  • Run the plugin by clicking Plugins->Styles Generator->Generate Shared Styles, or by using the ctrl+cmd+s shortcut
  • A log will recap what has been generated/updated

Styles Generator Usage

FAQ

What happens if my selection includes symbols, or artboards?

Sketch Styles Generator will ignore them.

How to generate shared styles for grouped layers?

Sketch Styles Generator doesn't recursively search for layers nested in one, or multiple groups. You can check the Sketch's native Select Group's Content on Click feature and refine your selection.

Can I use other groups/artboards/pages to generate the names?

No. This is an intentional choice to keep the scope of the plugin as narrow as possible, simplify its maintenance, and avoid duplication of features already available in other plugins.

How does this plugin manage updates, and already existing styles?

Below is a quick overview of how the plugin works behind the scenes. Please note that this is a generator, not a manager. 😜

The layer has no shared style applied, and no existing shared style matches its name: Create a new shared style

The layer has no shared style applied, but there's a shared style that shares its name: Apply the shared style to the layer

The layer has a shared style applied, and its synced, but there's a mismatch between the names: The shared style is renamed to match the layer

The layer was changed, and is now out-of-sync with the shared style applied to it: The shared style, and all its instances are synced

The layer was changed in both its appereance, and name, but still connected to a shared style: The shared style, and all its instances are synced and renamed

Integrations

Sketch Styles Generator is now fully integrated with Sketch Runner, the ultimate tool to speed up your Sketch workflow. You can trigger the plugin by simply typing its name.

Sketch Runner Integration

 

Download Details:

Author: lucaorio
Source Code: https://github.com/lucaorio/sketch-styles-generator 
License: MIT license

#sketch #style #generator #app 

Generate Hundreds Of Sketch Shared Styles in A Matter Of Seconds