├── .babelrc
├── .editorconfig
├── .eslintrc
├── .gitignore
├── .postcssrc
├── README.md
├── package.json
├── public
├── favicon.ico
└── index.html
├── src
├── App.vue
├── components
│ ├── DataList.js
│ ├── DataModel.js
│ ├── PostList.vue
│ └── mixins
│ │ └── query.js
└── main.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@vue/app"
4 | ]
5 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | indent_size = 2
9 | indent_style = space
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | trim_trailing_whitespace = false
15 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": [
4 | "@avalanche/eslint-config",
5 | "plugin:vue/recommended"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Numerous always-ignore extensions
2 | *.diff
3 | *.err
4 | *.log
5 | *.orig
6 | *.rej
7 | *.swo
8 | *.swp
9 | *.tgz
10 | *.vi
11 | *.zip
12 | *~
13 |
14 | # OS or Editor folders
15 | ._*
16 | .cache
17 | .DS_Store
18 | .idea
19 | .project
20 | .settings
21 | .tmproj
22 | *.esproj
23 | *.sublime-project
24 | *.sublime-workspace
25 | nbproject
26 | Thumbs.db
27 |
28 | # Folders to ignore
29 | dist/*
30 | !dist/_redirects
31 | node_modules
32 |
--------------------------------------------------------------------------------
/.postcssrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": {
3 | "autoprefixer": {}
4 | }
5 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Building Renderless Components to Handle CRUD Operations in Vue.js
2 |
3 | [](https://www.patreon.com/maoberlehner)
4 | [](https://paypal.me/maoberlehner)
5 |
6 | This is an example project for the following article: [Building Renderless Components to Handle CRUD Operations in Vue](https://markus.oberlehner.net/blog/building-renderless-components-to-handle-crud-operations-in-vue/)
7 |
8 | ## Build Setup
9 |
10 | ```bash
11 | # Install dependencies.
12 | npm install
13 |
14 | # Serve with hot reload.
15 | npm run serve
16 |
17 | # Build for production with minification.
18 | npm run build
19 | ```
20 |
21 | ## About
22 |
23 | ### Author
24 |
25 | Markus Oberlehner
26 | Website: https://markus.oberlehner.net
27 | Twitter: https://twitter.com/MaOberlehner
28 | PayPal.me: https://paypal.me/maoberlehner
29 | Patreon: https://www.patreon.com/maoberlehner
30 |
31 | ### License
32 |
33 | MIT
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "building-renderless-components-to-handle-crud-operations-in-vue",
3 | "version": "0.1.0",
4 | "author": "Markus Oberlehner",
5 | "homepage": "https://github.com/maoberlehner/building-renderless-components-to-handle-crud-operations-in-vue",
6 | "license": "MIT",
7 | "private": true,
8 | "scripts": {
9 | "serve": "vue-cli-service serve --open",
10 | "build": "vue-cli-service build",
11 | "lint": "vue-cli-service lint"
12 | },
13 | "dependencies": {
14 | "axios": "^0.18.0",
15 | "vue": "^2.5.16"
16 | },
17 | "devDependencies": {
18 | "@avalanche/eslint-config": "^2.0.0",
19 | "@vue/cli-plugin-babel": "^3.0.0-beta.10",
20 | "@vue/cli-plugin-eslint": "^3.0.0-beta.10",
21 | "@vue/cli-service": "^3.0.0-beta.10",
22 | "eslint-plugin-import": "^2.12.0",
23 | "eslint-plugin-vue": "^4.5.0",
24 | "node-sass": "^4.7.2",
25 | "sass-loader": "^6.0.6",
26 | "vue-template-compiler": "^2.5.13"
27 | },
28 | "browserslist": [
29 | "> 1%",
30 | "last 2 versions",
31 | "not ie <= 8"
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maoberlehner/building-renderless-components-to-handle-crud-operations-in-vue/16aa02ddd7428053d1ec0240081ffda43f4f5ecf/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Building Renderless Components to Handle CRUD Operations in Vue.js
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Vue.js Renderless Components CRUD Components
4 |
5 |
9 |
13 |
Post list
14 |
User
15 |
23 |
24 |
25 |
Loading...
26 |
Error while fetching data!
27 |
32 |
33 |
34 |
35 |
36 |
40 |
44 |
Single post
45 |
48 |
Loading...
49 |
{{ post.title }}
50 |
{{ post.body }}
51 |
52 |
53 |
54 |
55 |
56 |
60 |
Create new post
61 |
62 |
Loading...
63 |
64 |
65 | {{ post.title }}
66 | {{ post.body }}
67 |
68 |
69 |
92 |
93 |
94 |
95 |
96 |
100 |
104 |
Edit existing post
105 |
109 |
Loading...
110 |
111 |
{{ post.title }}
112 |
{{ post.body }}
113 |
114 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
167 |
--------------------------------------------------------------------------------
/src/components/DataList.js:
--------------------------------------------------------------------------------
1 | import queryMixin from './mixins/query';
2 |
3 | export default {
4 | mixins: [queryMixin],
5 | props: {
6 | // Provide a filter to limit the
7 | // results of the API request.
8 | filter: {
9 | type: Object,
10 | },
11 | },
12 | watch: {
13 | // Load the data from the given endpoint
14 | // on initial rendering of the component and
15 | // every time the filter property changes.
16 | filter: {
17 | immediate: true,
18 | handler: `load`,
19 | },
20 | },
21 | methods: {
22 | load() {
23 | return this.query(`get`, this.endpoint, { params: this.filter });
24 | },
25 | },
26 | render() {
27 | // Render the default scoped slot and
28 | // provide data and method properties
29 | // via the slot scope.
30 | return this.$scopedSlots.default({
31 | data: this.data,
32 | error: this.error,
33 | load: this.load,
34 | loading: this.loading,
35 | });
36 | },
37 | };
38 |
--------------------------------------------------------------------------------
/src/components/DataModel.js:
--------------------------------------------------------------------------------
1 | import queryMixin from './mixins/query';
2 |
3 | export default {
4 | mixins: [queryMixin],
5 | props: {
6 | // Make it possible to (optinally) provide
7 | // initial data via an entity property.
8 | entity: {
9 | type: Object,
10 | },
11 | // By providing an initial ID, we can link
12 | // the model instance with a specific record.
13 | id: {
14 | type: [Number, String],
15 | },
16 | },
17 | data() {
18 | return {
19 | data: this.entity || null,
20 | };
21 | },
22 | created() {
23 | // If an ID but no initial data entity
24 | // was provided, the specified record
25 | // is fetched from the API.
26 | if (this.id && !this.data) this.find();
27 | },
28 | methods: {
29 | create(data) {
30 | return this.query(`post`, this.endpoint, data);
31 | },
32 | destroy() {
33 | return this.query(`delete`, `${this.endpoint}/${this.id}`);
34 | },
35 | find() {
36 | return this.query(`get`, `${this.endpoint}/${this.id}`);
37 | },
38 | update(data) {
39 | return this.query(`patch`, `${this.endpoint}/${this.id}`, data);
40 | },
41 | },
42 | render() {
43 | return this.$scopedSlots.default({
44 | create: this.create,
45 | data: this.data,
46 | destroy: this.destroy,
47 | loading: this.loading,
48 | update: this.update,
49 | });
50 | },
51 | };
52 |
--------------------------------------------------------------------------------
/src/components/PostList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
No posts!
4 |
5 | -
10 |
16 |
17 |
{{ post.title }}
18 |
{{ post.body }}
19 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
50 |
--------------------------------------------------------------------------------
/src/components/mixins/query.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | export default {
4 | props: {
5 | baseUrl: {
6 | type: String,
7 | // The JSONPlaceholder API is a fake API
8 | // basically a Lorem Ipsum JSON API.
9 | default: `https://jsonplaceholder.typicode.com`,
10 | },
11 | endpoint: {
12 | type: String,
13 | required: true,
14 | },
15 | },
16 | data() {
17 | return {
18 | // Create a new axios instance.
19 | // See: https://github.com/axios/axios#creating-an-instance
20 | api: axios.create({ baseURL: this.baseUrl }),
21 | data: null,
22 | error: null,
23 | loading: false,
24 | };
25 | },
26 | methods: {
27 | // The `query` method will handle
28 | // different query types for us.
29 | async query(type, ...params) {
30 | // If we're currently loading content
31 | // we don't submit an additional request.
32 | if (this.loading) return;
33 |
34 | this.loading = true;
35 | try {
36 | const response = await this.api[type](...params);
37 | this.data = response.data;
38 | this.error = null;
39 | this.$emit(`success`, response);
40 | } catch (error) {
41 | this.data = null;
42 | this.error = error.response;
43 | this.$emit(`error`, error);
44 | }
45 | this.loading = false;
46 | },
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import App from './App.vue';
3 |
4 | Vue.config.productionTip = false;
5 |
6 | new Vue({
7 | render: h => h(App),
8 | }).$mount(`#app`);
9 |
--------------------------------------------------------------------------------