├── dist └── .gitkeep ├── .gitignore ├── demo ├── webpack.config.js ├── index.js └── index.html ├── LICENSE ├── package.json ├── README.md └── src └── index.js /dist/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/*.js 3 | -------------------------------------------------------------------------------- /demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | 3 | module.exports = { 4 | entry: './index.js', 5 | devtool: 'inline-source-map', 6 | module: { 7 | loaders: [ 8 | { test: /\.js$/, loader: 'buble-loader', exclude: /node_modules/ } 9 | ] 10 | }, 11 | output: { 12 | filename: './demo-bundle.js' 13 | }, 14 | resolve: { 15 | alias: { 16 | vue: 'vue/dist/vue.js' 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Paul Collett 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueMasonry from '../src/index'; 3 | import VueDummy from 'vue-dummy'; 4 | import DummyJS from 'dummyjs'; 5 | 6 | Vue.use(VueMasonry); 7 | Vue.use(VueDummy); 8 | 9 | const randomColsOptions = [ 10 | { default: 4, 1000: 3, 700: 2, 400: 1 }, 11 | { default: 3, 900: 2, 600: 1 }, 12 | { default: 5, 1000: 4, 800: 3, 500: 2, 400: 1 } 13 | ]; 14 | 15 | // Define a new component called button-counter 16 | Vue.component('dynamically-generated-masonry', { 17 | methods: { 18 | onResetClick() { 19 | this.$forceUpdate(); 20 | } 21 | }, 22 | render: function (createElement) { 23 | const childElements = Array(10).fill().map((v, i) => createElement( 24 | 'div', 25 | {class: 'item'}, 26 | `Item: #${i}: ` + DummyJS.text() 27 | )); 28 | 29 | const resetButton = createElement('button', { 30 | style: { 31 | marginBottom: '10px' 32 | }, 33 | on: { 34 | click: this.onResetClick 35 | } 36 | }, 'Rebuild Dynamic Layout'); 37 | 38 | const colOptions = randomColsOptions[Math.floor(Math.random()*randomColsOptions.length)]; 39 | 40 | const masonry = createElement( 41 | 'masonry', 42 | { 43 | props: { 44 | cols: colOptions, 45 | gutter: { default: '30px', 700: '20px' } 46 | } 47 | }, 48 | childElements 49 | ); 50 | 51 | return createElement('div', {}, [resetButton, masonry]); 52 | }, 53 | }) 54 | 55 | new Vue({ 56 | el: '#app' 57 | }); 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-masonry-css", 3 | "version": "1.0.3", 4 | "description": "Vue Masonry component powered by CSS, dependancy free", 5 | "main": "dist/vue-masonry.common.js", 6 | "module": "dist/vue-masonry.es2015.js", 7 | "unpkg": "dist/vue-masonry.min.js", 8 | "jsdelivr": "dist/vue-masonry.min.js", 9 | "scripts": { 10 | "build": "node ./build.js && npm run build:demo", 11 | "build:demo": "cd ./demo && webpack", 12 | "dev": "cd ./demo && webpack-dev-server --progress --colors", 13 | "preversion": "npm run build", 14 | "prepublishOnly": "npm run build", 15 | "publish-package": "git push && git push --tags && npm publish --registry=https://registry.npmjs.org/" 16 | }, 17 | "files": [ 18 | "dist" 19 | ], 20 | "keywords": [ 21 | "vue", 22 | "vue.js", 23 | "masonry", 24 | "masonary", 25 | "component", 26 | "css" 27 | ], 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/paulcollett/vue-masonry-css.git" 31 | }, 32 | "author": "Paul Collett", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/paulcollett/vue-masonry-css/issues" 36 | }, 37 | "homepage": "https://github.com/paulcollett/vue-masonry-css#readme", 38 | "devDependencies": { 39 | "buble-loader": "^0.4.1", 40 | "rollup": "^0.50.0", 41 | "rollup-plugin-buble": "^0.16.0", 42 | "uglify-js": "^3.1.3", 43 | "vue": "^2.5.16", 44 | "vue-dummy": "^1.1.1", 45 | "webpack": "^3.6.0", 46 | "webpack-dev-server": "^2.9.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vue Masonry Css Example 6 | 7 | 8 | 25 | 26 | 27 | 28 | ← GitHub / Docs 29 |

vue-masonry-css example

30 |

A Vue masonry component powered by CSS to rendered fast, responsive and to be free of jQuery or other dependencies. Build specifically for Vue projects.

31 | 32 |
33 | 37 |
38 |

Item: {{index + 1}}

39 | 45 |

46 |
47 |
48 |
49 |

Dynamic Masonry Layout showing re-rendering:

50 | 51 |
52 |

Example with static HTML (see source):

53 | 57 |
My Element 1
58 |
My Element 2
59 |
My Element 3
With another line
60 |
My Element 4
61 |
My Element 5
62 |
My Element 6
63 |
My Element 7
64 |
My Element 8
65 |
66 |
67 |

Example with a mix of static HTML and dynamically rendered (see source):

68 |

Also, a smaller gutter size

69 | 73 |
My Element 1
74 |
My Element 2
75 |
Item: {{index + 1 + 2}}
76 |
My Element 6
77 |
My Element 7
78 |
My Element 8
79 |
80 |
81 |

Placeholder text and images rendered with Vue-Dummy

82 |
83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A new masonry component powered by CSS to be fast loading and free of jQuery or other dependencies. Build specifically for Vue.js projects. 2 | 3 | ![Vue Masonry](https://user-images.githubusercontent.com/1904774/31857149-d226f492-b68a-11e7-9f8c-5148f0dca74d.png) 4 | 5 | ### 😎 Why? 6 | 7 | Existing solutions like Vue wrapped DeSandro Masonry, while popular, don't actually leverage Vue's highly optimized Virtual DOM renderer and in DeSandro Masonry's case, actually renders elements twice before showing the layout. All of this is ok but we found it to lead to a slow, "laggy" user experience that would occasionally miss-render our layout. 8 | 9 | Our need for a simple Masonry layout that was fast, used Vue's Virtual DOM without needing jQuery or other dependencies led us to explore what we could do with the latest techniques using just CSS within a React Component. 10 | 11 | Between flexbox, css columns, css grid we settled on plain ol' div's and a dab of flexbox that allows for "fluid" responsive layouts by default but most importantly is true to Vue's rendering lifecycle. 12 | 13 | *`vue-masonry-css`* Is a Vue Component with a simple interface to order items into the desired columns at specified breakpoints. With minimal CSS this leads to a quick, reliable solution that also has great browser support along with fast rendering performance ..just as Vue.js intended. 14 | 15 | 😄 What does this do 16 | - Responsive! ..always 17 | - IE 10+ CSS Support (and, IE9) 18 | - No Dependencies - Which means no need for jQuery! 19 | - Works with existing CSS animations on your elements, like fading in on first load 20 | - CSS powered (Faster to render) 21 | - Allows for Gaps (Gutters) between elements 22 | 23 | 🏳️ What doesn't this do 24 | - Works with elements with different widths 25 | - Sorting based on height - This kills performance, so if you don't need it we're here for you 26 | 27 | ### 😲 Simple Usage 28 | 29 | Add `vue-masonry-css` to your project: 30 | 31 | By script.. 32 | 33 | ```HTML 34 | 35 | 36 | ``` 37 | 38 | Or as a module... `npm install vue-masonry-css --save-dev` 39 | 40 | ```JS 41 | import Vue from 'vue' 42 | import VueMasonry from 'vue-masonry-css' 43 | 44 | Vue.use(VueMasonry); 45 | ``` 46 | 47 | In your HTML template... 48 | ```HTML 49 | 53 |
Item: {{index + 1}}
54 |
55 | ``` 56 | 57 | ### Resposive Breakpoints 58 | 59 | Different columns and gutter sizes can be specified by passing an object containing key's of the window widths and their values representing the number of columns or gutter size. To have a fallback value, use the `default` key. 60 | 61 | _note:_ The `cols=` attribute needs to use Vues bind method to evaluate objects. Instead of `cols=""` use either `v-bind:cols="{ 700: 3 }"` or the shorthand `:cols="{ 700: 3 }"` 62 | 63 | ```HTML 64 | 68 |
Item: {{index + 1}}
69 |
70 | ``` 71 | 72 | In the above example, the number of columns will default to 4. When the window's is between 1000px and 700px, the number of columns will be 3. The key represents the `max-width` of the window, and `vue-masonry-css` will use the largest key that satisfies this. 73 | 74 | ### Example 75 | 76 | https://paulcollett.github.io/vue-masonry-css/demo/ 77 | 78 | ### Suggestions & Issues 79 | https://github.com/paulcollett/vue-masonry-css 80 | 81 | **Contact me direct:** 82 | * https://github.com/paulcollett 83 | * http://paulcollett.com 84 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // the component name `` 2 | // can be overridden with `Vue.use(Masonry, { name: 'the-masonry' });` 3 | const componentName = 'masonry'; 4 | 5 | const props = { 6 | tag: { 7 | type: [String], 8 | default: 'div' 9 | }, 10 | cols: { 11 | type: [Object, Number, String], 12 | default: 2 13 | }, 14 | gutter: { 15 | type: [Object, Number, String], 16 | default: 0 17 | }, 18 | css: { 19 | type: [Boolean], 20 | default: true 21 | }, 22 | columnTag: { 23 | type: [String], 24 | default: 'div' 25 | }, 26 | columnClass: { 27 | type: [String, Array, Object], 28 | default: () => [] 29 | }, 30 | columnAttr: { 31 | type: [Object], 32 | default: () => ({}) 33 | } 34 | }; 35 | 36 | // Get the resulting value from `:col=` prop 37 | // based on the window width 38 | const breakpointValue = (mixed, windowWidth) => { 39 | const valueAsNum = parseInt(mixed); 40 | 41 | if(valueAsNum > -1) { 42 | return mixed; 43 | }else if(typeof mixed !== 'object') { 44 | return 0; 45 | } 46 | 47 | let matchedBreakpoint = Infinity; 48 | let matchedValue = mixed.default || 0; 49 | 50 | for(let k in mixed) { 51 | const breakpoint = parseInt(k); 52 | const breakpointValRaw = mixed[breakpoint]; 53 | const breakpointVal = parseInt(breakpointValRaw); 54 | 55 | if(isNaN(breakpoint) || isNaN(breakpointVal)) { 56 | continue; 57 | } 58 | 59 | const isNewBreakpoint = windowWidth <= breakpoint && breakpoint < matchedBreakpoint; 60 | 61 | if(isNewBreakpoint) { 62 | matchedBreakpoint = breakpoint; 63 | matchedValue = breakpointValRaw; 64 | } 65 | } 66 | 67 | return matchedValue; 68 | } 69 | 70 | const component = { 71 | props, 72 | 73 | data() { 74 | return { 75 | displayColumns: 2, 76 | displayGutter: 0 77 | } 78 | }, 79 | 80 | mounted() { 81 | this.$nextTick(() => { 82 | this.reCalculate(); 83 | }); 84 | 85 | // Bind resize handler to page 86 | if(window) { 87 | window.addEventListener('resize', this.reCalculate); 88 | } 89 | }, 90 | 91 | updated() { 92 | this.$nextTick(() => { 93 | this.reCalculate(); 94 | }); 95 | }, 96 | 97 | beforeDestroy() { 98 | if(window) { 99 | window.removeEventListener('resize', this.reCalculate); 100 | } 101 | }, 102 | 103 | methods: { 104 | // Recalculate how many columns to display based on window width 105 | // and the value of the passed `:cols=` prop 106 | reCalculate() { 107 | const previousWindowWidth = this.windowWidth; 108 | 109 | this.windowWidth = (window ? window.innerWidth : null) || Infinity; 110 | 111 | // Window resize events get triggered on page height 112 | // change which when loading the page can result in multiple 113 | // needless calculations. We prevent this here. 114 | if(previousWindowWidth === this.windowWidth) { 115 | return; 116 | } 117 | 118 | this._reCalculateColumnCount(this.windowWidth); 119 | 120 | this._reCalculateGutterSize(this.windowWidth); 121 | }, 122 | 123 | _reCalculateGutterSize(windowWidth) { 124 | this.displayGutter = breakpointValue(this.gutter, windowWidth); 125 | }, 126 | 127 | _reCalculateColumnCount(windowWidth) { 128 | let newColumns = breakpointValue(this.cols, windowWidth); 129 | 130 | // Make sure we can return a valid value 131 | newColumns = Math.max(1, Number(newColumns) || 0); 132 | 133 | this.displayColumns = newColumns; 134 | }, 135 | 136 | _getChildItemsInColumnsArray() { 137 | const columns = []; 138 | let childItems = this.$slots.default || []; 139 | 140 | // This component does not work with a child ..yet, 141 | // so for now we think it may be helpful to ignore until we can find a way for support 142 | if(childItems.length === 1 && childItems[0].componentOptions && childItems[0].componentOptions.tag == 'transition-group') { 143 | childItems = childItems[0].componentOptions.children; 144 | } 145 | 146 | // Loop through child elements 147 | for (let i = 0, visibleItemI = 0; i < childItems.length; i++, visibleItemI++) { 148 | // skip Vue elements without tags, which includes 149 | // whitespace elements and also plain text 150 | if(!childItems[i].tag) { 151 | visibleItemI--; 152 | 153 | continue; 154 | } 155 | 156 | // Get the column index the child item will end up in 157 | const columnIndex = visibleItemI % this.displayColumns; 158 | 159 | if(!columns[columnIndex]) { 160 | columns[columnIndex] = []; 161 | } 162 | 163 | columns[columnIndex].push(childItems[i]); 164 | } 165 | 166 | return columns; 167 | } 168 | }, 169 | 170 | render(createElement) { 171 | const columnsContainingChildren = this._getChildItemsInColumnsArray(); 172 | const isGutterSizeUnitless = parseInt(this.displayGutter) === this.displayGutter * 1; 173 | const gutterSizeWithUnit = isGutterSizeUnitless ? `${this.displayGutter}px` : this.displayGutter; 174 | 175 | const columnStyle = { 176 | boxSizing: 'border-box', 177 | backgroundClip: 'padding-box', 178 | width: `${100 / this.displayColumns}%`, 179 | border: '0 solid transparent', 180 | borderLeftWidth: gutterSizeWithUnit 181 | }; 182 | 183 | const columns = columnsContainingChildren.map((children, index) => { 184 | /// Create column element and inject the children 185 | return createElement(this.columnTag, { 186 | key: index + '-' + columnsContainingChildren.length, 187 | style: this.css ? columnStyle : null, 188 | class: this.columnClass, 189 | attrs: this.columnAttr 190 | }, children); // specify child items here 191 | }); 192 | 193 | const containerStyle = { 194 | display: ['-webkit-box', '-ms-flexbox', 'flex'], 195 | marginLeft: `-${gutterSizeWithUnit}` 196 | }; 197 | 198 | // Return wrapper with columns 199 | return createElement( 200 | this.tag, // tag name 201 | this.css ? { style: containerStyle } : null, // element options 202 | columns // column vue elements 203 | ); 204 | } 205 | }; 206 | 207 | const Plugin = function () {} 208 | 209 | Plugin.install = function (Vue, options) { 210 | if (Plugin.installed) { 211 | return; 212 | } 213 | 214 | if(options && options.name) { 215 | Vue.component(options.name, component); 216 | } else { 217 | Vue.component(componentName, component); 218 | } 219 | } 220 | 221 | if (typeof window !== 'undefined' && window.Vue) { 222 | window.Vue.use(Plugin); 223 | } 224 | 225 | export default Plugin; 226 | --------------------------------------------------------------------------------