<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h1 class="text-center">Responsive Tiles Vue Component Example</h1>
<div id="app"><span class="loader">Loading...</span></div>
<div id="footer">Images courtesy of <a href="https://flickr.com/photos/janumedia">I Nengah Januartha</a></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.5/require.min.js"></script>
<script src="script.js"></script>
</body>
</html>
(function()
{
require.config(
{
paths:{
Vue: "https://vuejs.org/js/vue",
vue: "https://rawgit.com/edgardleal/require-vuejs/master/dist/require-vuejs"
/*vue: "https://cdn.rawgit.com/edgardleal/require-vuejs/aeaff6db/dist/require-vuejs.min"*/
}
});
require(["vue!Tiles", "data.json?callback=define"],
function(Tiles, data)
{
new Tiles(document.getElementById("app"), data.list);
});
})();
# Responsive Tiles Grid Vue Component
A Tiles Grid VueJS component and relying to RequireJS as module and data (json) loader
# Features
- Responsive
- 2 Columns Tiles
- Centering last row when only has single item
- JSON data
# Libraries
- VueJS
- RequireJS
# Where to use?
Any website project that use client browser rendering
<template>
<div id="container">
<ul id="tiles-container" class="table">
<li v-for="(item, index) in list" :class="getClass(index)">
<div>
<img :src="item.img">
</div>
<div class="info">
<h2>{{ item.name }}</h2>
<div v-html="item.desc"></div>
<br>
<div class="button-group">
<button>More Info</button>
</div>
</div>
</li>
</ul>
</div>
</template>
<script>
define(["Vue"], function(Vue)
{
return function(el, list)
{
Vue.component("tiles",
{
template: template,
props:
{
list: {
type:Array,
required: true
}
},
mounted: function()
{
//listen to resize event, make sure tile size follow the window size
if(window.addEventListener)
{
window.addEventListener("resize", this.adjustHeight);
} else
{
window.attachEvent("onresize", this.adjustHeight);
}
//also important to listen image onload as will resize the tile
var self = this;
var images = document.querySelectorAll("#tiles-container li img");
[].slice.call(images).forEach(function(img)
{
img.onload = function(e)
{
self.adjustHeight();
}
});
//initial set
this.adjustHeight();
},
methods:
{
getClass: function(index)
{
var len = this.list.length;
if(len%2 == 0 && index == len-2)
{
return "last-2nd";
} else if (len%2 == 1 && index == len-1)
{
return "last-center";
}
return "";
},
adjustHeight: function()
{
var tiles = document.querySelectorAll("#tiles-container > li"),
buttonStyle, maxH = 0;
//reset to allow autosize
[].slice.call(tiles).forEach(function(tile)
{
tile.style.height = "";
buttonStyle = tile.querySelector(".button-group").style;
buttonStyle.position = "relative";
});
//get max Height
[].slice.call(tiles).forEach(function(tile)
{
maxH = Math.max(maxH, tile.offsetHeight);
});
//implement max Height to all tiles
[].slice.call(tiles).forEach(function(tile)
{
tile.style.height = maxH + "px";
buttonStyle = tile.querySelector(".button-group").style;
buttonStyle.position = "absolute";
buttonStyle.bottom = "12px";
buttonStyle.left = 0;
buttonStyle.right = 0;
});
}
}
});
//render component
el.innerHTML = "<tiles :list='listArr'></tiles>";
var app = new Vue(
{
el: el,
data:
{
listArr: list
}
});
}
});
</script>
define({
list: [{
name: "Sanur",
desc: "Sunrise at Bali's ealiest beach resorts",
img: "https://c2.staticflickr.com/6/5327/8975420033_9eeb1aed1f.jpg"
}, {
name: "Tanah Lot",
desc: "One of most popular tourist destination in the world",
img: "https://c1.staticflickr.com/5/4093/4785053162_0c990c21d1.jpg"
}/*, {
name: "Yeh Embang",
desc: "Mount Batukaru from Yeh Embang, Jembrana Bali",
img: "https://c2.staticflickr.com/2/1231/4726659769_9e56963d80.jpg"
}*/, {
name: "Karang Bolong",
desc: "Majesty of Karang Bolong at Yeh Gangga Beach",
img: "https://c1.staticflickr.com/5/4137/5221388030_8ef60c71b9.jpg"
}]
})
/** Variables **/
$body-bg-color: lightgrey;
$tile-bg-color: white;
$button-color: grey;
.transition {
-webkit-transition-duration: 0.4s;
-moz-transition-duration: 0.4s;
-o-transition-duration: 0.4s;
transition-duration: 0.4s;
}
@mixin width-calc {
width: -webkit-calc(50% - 20px);
width: -moz-calc(50% - 20px);
width: calc(50% - 20px);
}
body {
font-family: sans-serif;
font-size: 16px;
margin: 0 auto;
padding: 0;
background-color: $body-bg-color;
}
h1 {
font-size: 1.2em;
}
.text-center {
text-align:center;
}
.table {
&:before,
&:after {
content: "";
display: table;
}
&:after {
clear: both;
}
}
.loader {
@extend .text-center;
}
/** TILES **/
#tiles-container {
max-width: 900px;
margin: 0 auto;
padding: 0 20px;
li {
@include width-calc;
background-color: $tile-bg-color;
margin-top: 0px;
margin-left: 20px;
margin-right: 0px;
margin-bottom: 40px;
float: left;
position: relative;
list-style: none;
&:nth-child(odd) {
margin-left: 0;
margin-right: 20px;
}
@media only screen and (max-width:560px) {
width: 100%;
margin-top: 0px;
margin-left: 0px;
margin-right: 0px;
margin-bottom: 40px;
&:nth-child(odd){
margin-left: 0;
margin-right: 0;
}
&:first-child,
&:nth-last-child(2) {
margin-bottom: 40px;
}
}
@media only screen and (max-width:350px) {
margin-bottom: 20px;
}
}
li:last-child,
li.last-2nd {
margin-bottom: 0px;
@media only screen and (max-width:560px) {
margin-bottom: 40px;
}
}
li.last-center {
margin-left: 25%;
margin-right: 25%;
@media only screen and (max-width:560px) {
margin-left: 0;
margin-right: 0;
margin-bottom: 40px;
}
}
img {
width: 100%;
}
.info {
padding: 20px;
text-align: center;
}
h2 {
font-size: 1.2em;
}
button {
border: 1px solid $button-color;
border-radius:0;
background-color: inherit;
padding: 8px 16px;
margin-bottom: 14px;
color: $button-color;
font-size: 0.6em;
text-transform: uppercase;
letter-spacing: 0.05em;
cursor: pointer;
@extend .transition;
&:hover {
background-color:$button-color;
color:white;
}
}
}
#footer {
margin: 20px auto;
text-align: center;
font-size: 0.6em;
color: grey;
a {
text-decoration: none;
outline: none;
color: inherit;
&:hover {
text-decoration: underline;
}
}
}