Creating a reusable Vuetify component to use in other apps Andy Lee Published: February 5, 2019 Our newly introduced Meeting Scheduler UI Tool uses the popular Vue framework to power its single-page app. The Meeting Scheduler helps application developers prompt users to set up meetings with one another.Our engineers appreciate Vue’s simplicity, growing adoption, and robust ecosystem. The Meeting Scheduler requires several UI components, so we searched for a library to assist with including common widgets such as date-pickers, buttons, and input fields. We chose to use the Vuetify library since we like the material design and built-in components it offers.Distributing apps built with VuetifyThe Meeting Scheduler is designed to be included in other web applications. However, our choice of Vuetify presents a problem. Vuetify stylesheets pollute global styles and cause a high chance of interfering with the layout of the page they are included in. Here is an excerpt from CSS within Vuetify: /* from vuetify.css */ html { box-sizing: border-box; overflow-y: scroll; /* All browsers without overlaying scrollbars */ -webkit-text-size-adjust: 100%; /* iOS 8+ */ } *, ::before, ::after { box-sizing: inherit; } /* ... */ a { background-color: transparent; /* Remove the gray background on active links in IE 10 */ -webkit-text-decoration-skip: objects; /* Remove gaps in link underlines in iOS 8+ and Safari 8+ */ }12345678910111213141516/* from vuetify.css */html { box-sizing: border-box; overflow-y: scroll; /* All browsers without overlaying scrollbars */ -webkit-text-size-adjust: 100%; /* iOS 8+ */}*,::before,::after { box-sizing: inherit;}/* ... */a { background-color: transparent; /* Remove the gray background on active links in IE 10 */ -webkit-text-decoration-skip: objects; /* Remove gaps in link underlines in iOS 8+ and Safari 8+ */}As you can see, Vuetify stylesheets contain global element selectors such as html, *, ::before, and ::after that affect the whole page.To allow developers to embed our library, we need to ensure that our styles only apply to our own component and don’t impact other elements on the page. There are a couple of options to address this issue.Use an iframeAn iframe creates a separate page scope, so styles in a component included within an iframe won’t interact with the parent page. However, this also limits the interactions between the component and the parent page, especially if the component is hosted on a different domain. Since our component in this case also optionally launches in a modal, an iframe is not an option.Use Web ComponentsWeb Components are a new standard to create reusable components. However, the community is continuing to discuss the concept of a local CSS module. For the present time, developers must embed style tags into component templates to include custom styling. Furthermore, it isn’t straightforward to include inline styles in our Webpack build setup. Web Components also require native ES2015 support, which mean end-users using IE11 and Edge are out of luck. We prefer to continue to support these browsers at the present time.Prefix a master class to each CSS selectorWe can create the same effect as a local CSS scope by programmatically wrapping each component inside a root element, and prefixing all CSS selectors with that root element’s class name. This requires additional post-processing to ensure all dependencies’ stylesheets are wrapped, but it provides the best browser compatibility of all the options above. The build process can pre-process all dependencies’ stylesheets.The Meeting Scheduler CSS PrefixWe wrap the Meeting Scheduler component with a div element that includes a class named kloudless-meeting-scheduler as shown below: <div id="container"> <div class="kloudless-meeting-scheduler"> <!-- component HTML goes here --> </div> </div>12345<div id="container"> <div class="kloudless-meeting-scheduler"> <!-- component HTML goes here --> </div></div>The next step is to process all CSS files and search for selectors. Prefix each line containing a selector with the master class. However, there are some exceptions to watch out for. Consider the styles below: /* from vuetify.css */ @media only screen and (max-width: 599px) { .v-bottom-sheet.v-dialog.v-bottom-sheet--inset { max-width: none; } } @-webkit-keyframes shake { 59% { margin-left: 0; } 60%, 80% { margin-left: 2px; } 70%, 90% { margin-left: -2px; } } html { box-sizing: border-box; overflow-y: scroll; /* All browsers without overlaying scrollbars */ -webkit-text-size-adjust: 100%; /* iOS 8+ */ } .v-input--has-state.error--text .v-label { -webkit-animation: shake 0.6s cubic-bezier(0.25, 0.8, 0.5, 1); animation: shake 0.6s cubic-bezier(0.25, 0.8, 0.5, 1); } button, [type="button"], [type="reset"], [type="submit"], [role="button"] { cursor: pointer; } /* from vuetify.css end */ @font-face { font-family: 'Roboto'; src: url("fonts/Roboto-Regular.woff") format("woff"), url("fonts/Roboto-Regular.woff2") format("woff2"); }123456789101112131415161718192021222324252627282930313233343536373839/* from vuetify.css */@media only screen and (max-width: 599px) { .v-bottom-sheet.v-dialog.v-bottom-sheet--inset { max-width: none; }}@-webkit-keyframes shake { 59% { margin-left: 0; } 60%, 80% { margin-left: 2px; } 70%, 90% { margin-left: -2px; }}html { box-sizing: border-box; overflow-y: scroll; /* All browsers without overlaying scrollbars */ -webkit-text-size-adjust: 100%; /* iOS 8+ */}.v-input--has-state.error--text .v-label { -webkit-animation: shake 0.6s cubic-bezier(0.25, 0.8, 0.5, 1); animation: shake 0.6s cubic-bezier(0.25, 0.8, 0.5, 1);}button,[type="button"],[type="reset"],[type="submit"],[role="button"] { cursor: pointer;}/* from vuetify.css end */@font-face { font-family: 'Roboto'; src: url("fonts/Roboto-Regular.woff") format("woff"), url("fonts/Roboto-Regular.woff2") format("woff2");}The snippet above highlights some exceptions:Replace the html selector with the master class instead of adding it as a prefix.Don’t prefix a line that begins with @font-face, @media, or similar.Include a prefix in the @keyframe directive itself for animation properties, and also create a local keyframe name.Don’t touch rules within the @keyframe block.Remember to handle blocks with multiple selectors separated by commas.Here is an algorithm based on the rules above:Look for lines that don’t begin with @ or end with { or ,.If the selector is html, replace it with MASTER_CLASS. Otherwise, prefix MASTER_CLASS.Look for lines beginning with @-webkit-keyframe or @keyframe.Prefix the keyframe name with MASTER_CLASS-.Ignore the following lines until reaching a line that only contains }.Look for lines with -webkit-animation or animation:Replace the keyframe name with MASTER_CLASS-{name}.Here’s an implementation in JavaScript: const outputCss = []; const MASTER_CLASS = 'kloudless-meeting-scheduler'; let isInsideKeyFrame = false; lineReader.on('line', (line) => { let nextLine; if (isInsideKeyFrame) { // don't change anything inside a keyframe nextLine = line; if (line.search(/^}/) === 0 ) { // leaving a keyframe definition. revert the flag isInsideKeyFrame = false; } } else { if (line.search(/^\s*[^@/]+(\s*\{|,)$/) === 0) { // if this line is a css selector if (line.search(/^html/) === 0) { // Replace html with the master class nextLine = line.replace(/^html/, `.${MASTER_CLASS}`); } else { // For other css selectors, prefix the master class to the selector nextLine = line.replace( /^(\s*)([^@]+)(\s*\{|,)$/, (_, p1, p2, p3) => `${p1}.${MASTER_CLASS} ${p2}${p3}`, ); } } else if (line.search(/^@(-webkit-)?keyframe/) === 0) { // beginning of a keyframe definition // set flag to true and prefix the keyframe name with ${MASTER_CLASS}- isInsideKeyFrame = true; nextLine = line.replace(/\s([a-z-]+)\s*{$/g, (_, p1) => ` ${MASTER_CLASS}-${p1} {`); } else if (line.search(/^\s*(-webkit-)?animation/) === 0) { // for animation properties, change the name to ${MASTER_CLASS}-${name} nextLine = line.replace(/(-webkit-)?animation: /, match => `${match}${MASTER_CLASS}-`); } else { nextLine = line; } } outputCss.push(nextLine); });123456789101112131415161718192021222324252627282930313233343536373839404142 const outputCss = [];const MASTER_CLASS = 'kloudless-meeting-scheduler';let isInsideKeyFrame = false;lineReader.on('line', (line) => { let nextLine; if (isInsideKeyFrame) { // don't change anything inside a keyframe nextLine = line; if (line.search(/^}/) === 0 ) { // leaving a keyframe definition. revert the flag isInsideKeyFrame = false; } } else { if (line.search(/^\s*[^@/]+(\s*\{|,)$/) === 0) { // if this line is a css selector if (line.search(/^html/) === 0) { // Replace html with the master class nextLine = line.replace(/^html/, `.${MASTER_CLASS}`); } else { // For other css selectors, prefix the master class to the selector nextLine = line.replace( /^(\s*)([^@]+)(\s*\{|,)$/, (_, p1, p2, p3) => `${p1}.${MASTER_CLASS} ${p2}${p3}`, ); } } else if (line.search(/^@(-webkit-)?keyframe/) === 0) { // beginning of a keyframe definition // set flag to true and prefix the keyframe name with ${MASTER_CLASS}- isInsideKeyFrame = true; nextLine = line.replace(/\s([a-z-]+)\s*{$/g, (_, p1) => ` ${MASTER_CLASS}-${p1} {`); } else if (line.search(/^\s*(-webkit-)?animation/) === 0) { // for animation properties, change the name to ${MASTER_CLASS}-${name} nextLine = line.replace(/(-webkit-)?animation: /, match => `${match}${MASTER_CLASS}-`); } else { nextLine = line; } } outputCss.push(nextLine);});Here is the result when applied to the styles above: @media only screen and (max-width: 599px) { .kloudless-meeting-scheduler .v-bottom-sheet.v-dialog.v-bottom-sheet--inset { max-width: none; } } @-webkit-keyframes kloudless-meeting-scheduler-shake { 59% { margin-left: 0; } 60%, 80% { margin-left: 2px; } 70%, 90% { margin-left: -2px; } } .kloudless-meeting-scheduler { box-sizing: border-box; overflow-y: scroll; /* All browsers without overlaying scrollbars */ -webkit-text-size-adjust: 100%; /* iOS 8+ */ } .kloudless-meeting-scheduler .v-input--has-state.error--text .v-label { -webkit-animation: kloudless-meeting-scheduler-shake 0.6s cubic-bezier(0.25, 0.8, 0.5, 1); animation: kloudless-meeting-scheduler-shake 0.6s cubic-bezier(0.25, 0.8, 0.5, 1); } .kloudless-meeting-scheduler button, .kloudless-meeting-scheduler [type="button"], .kloudless-meeting-scheduler [type="reset"], .kloudless-meeting-scheduler [type="submit"], .kloudless-meeting-scheduler [role="button"] { cursor: pointer; } @font-face { font-family: 'Roboto'; src: url("fonts/Roboto-Regular.woff") format("woff"), url("fonts/Roboto-Regular.woff2") format("woff2"); }12345678910111213141516171819202122232425262728293031323334353637@media only screen and (max-width: 599px) { .kloudless-meeting-scheduler .v-bottom-sheet.v-dialog.v-bottom-sheet--inset { max-width: none; }}@-webkit-keyframes kloudless-meeting-scheduler-shake { 59% { margin-left: 0; } 60%, 80% { margin-left: 2px; } 70%, 90% { margin-left: -2px; }}.kloudless-meeting-scheduler { box-sizing: border-box; overflow-y: scroll; /* All browsers without overlaying scrollbars */ -webkit-text-size-adjust: 100%; /* iOS 8+ */}.kloudless-meeting-scheduler .v-input--has-state.error--text .v-label { -webkit-animation: kloudless-meeting-scheduler-shake 0.6s cubic-bezier(0.25, 0.8, 0.5, 1); animation: kloudless-meeting-scheduler-shake 0.6s cubic-bezier(0.25, 0.8, 0.5, 1);}.kloudless-meeting-scheduler button,.kloudless-meeting-scheduler [type="button"],.kloudless-meeting-scheduler [type="reset"],.kloudless-meeting-scheduler [type="submit"],.kloudless-meeting-scheduler [role="button"] { cursor: pointer;}@font-face { font-family: 'Roboto'; src: url("fonts/Roboto-Regular.woff") format("woff"), url("fonts/Roboto-Regular.woff2") format("woff2");}Keep in mind that this is not a true local scope. For example, it is still possible to pollute page styles by using !important.ConclusionWe chose to both prefix a wrapper class during the build process as well as provide an iframe URL to developers. This allows most browsers to support our component while providing developers a quick way to embed our library without additional JavaScript.There are several caveats to implementing the hack shown above; we look forward to using Web Components in the near future once support is wide-spread.Check out our the final result, the open source Meeting Scheduler, for an easy way to prompt users to schedule meetings with one another.