- node.js version v16.15.0 (Download node.js)
Clone https://github.com/UCLALibrary/ucla-library-website-components.git
- Run
$ npm install
from a terminal to install dependencies - Run
$ npm run serve
to start a development server. - Open the browser and load
http://localhost:8080
which will serve dev/serve.vue page - Run
$ npm run storybook
to start a storybook server. - Run
$ npm run cypress
to open cypresss.
- Local: http://localhost:8080/
Note that the development build is not optimized.
To create a production build, run npm run build
.
Command | Description |
---|---|
npm run serve |
Starts a vue cli service server and serves dev/serve.vue page |
Connect the component to the library-website-nuxt site by adding to the test page in Nuxtpages/test_library/index.vue
- In the library-website-nuxt site Create a new branch of this branch,test-component-library-package
- Then in your terminal, in the
ucla-library-website-components
repo run:
$ ucla-library-website-components\🦖npm run build
+ This gives us 3 dist files + src/entry.esm.js → dist/ucla-library-website-components.esm.js...clean: postcss.plugin was deprecated. Migration guide: https://evilmartians.com/chronicles/postcss-8-plugin-migration created dist/ucla-library-website-components.esm.js in 1.9s + src/entry.js → dist/ucla-library-website-components.ssr.js... created dist/ucla-library-website-components.ssr.js in 1s + src/entry.js → dist/ucla-library-website-components.min.js... created dist/ucla-library-website-components.min.js in 1.5s - Then in your terminal, in the
ucla-library-website-components
repo run:
$ ucla-library-website-components\🦖npm link
- Then in your terminal, in the
library-website-nuxt
repo run:npm link ucla-library-website-components
- Add to
pages/test_library/index.vue
npm run dev - Open http://localhost:3000/test_library
How to move library-website-nuxt (nuxt project) components to ucla-library-website-components (vue 2 project)
Hi!
This readMe is to help you move components from nuxt to vue 2, specifically for UCLA library repositories.
Let’s start with the basics:
We needed to reuse the components in other projects, in this case, we needed to create a library of components to do that. The tech stack chosen was vue 2 with rollup.
Vue (pronounced /vjuː/, like view) is a progressive framework for building user interfaces. Unlike other monolithic frameworks, Vue is designed from the ground up to be incrementally adoptable. The core library is focused on the view layer only, and is easy to pick up and integrate with other libraries or existing projects. On the other hand, Vue is also perfectly capable of powering sophisticated Single-Page Applications when used in combination with modern tooling and supporting libraries
Rollup is a module bundler for JavaScript which compiles small pieces of code into something larger and more complex, such as a library or application. It uses the new standardized format for code modules included in the ES6 revision of JavaScript, instead of previous idiosyncratic solutions such as CommonJS and AMD. ES modules let you freely and seamlessly combine the most useful individual functions from your favorite libraries. This will eventually be possible natively everywhere, but Rollup lets you do it today.
You need access to https://github.com/UCLALibrary/library-website-nuxt for the nuxt repository and https://github.com/UCLALibrary/ucla-library-website-components for the vue 2 + rollup repository.
Now we need to understand the basics of the https://github.com/UCLALibrary/ucla-library-website-components project configuration.
First, we have 3 configurations:
- One for rollup (the bundler)
- One for storybook
- One for vue 2
With that in mind, we need to carefully test 3 “developing ambients”:
- dev server
- storybook server
- npm package
But first, let’s understand the basics of each configuration.
CSS/SCSS
css: {
loaderOptions: {
sass: {
additionalData: `
@import "@/styles/variables-scss.scss";
`,
},
},
},
This part of the vue.config.js
is for using variables-scss.scss
as an entry point for the css in the vue project, that way we can import https://github.com/UCLALibrary/design-tokens to use in the project.
SVG + node_modules
chainWebpack: (config) => {
config.module.rules.delete("svg")
},
configureWebpack: {
module: {
rules: [
{
oneOf: [
{
test: /\.(jpg|png|svg|gif)$/,
type: "asset/inline",
resourceQuery: /url/,
},
{
test: /\.svg$/,
loader: "vue-svg-loader",
options: {
svgo: {
plugins: [
{
removeViewBox: false,
},
],
},
},
},
],
},
],
},
},
This code allows us to use svg’s in the vue project, even if it’s from node_modules. We also got vue-svg-loader to make svg as components. One problem we were facing was using SVGs as background-image, or background in general. We solved it with the oneOf
rule.
One big problem we were facing also, was about the viewbox of the svg’s, later we understood that we needed to add svgo configuration. That’s why we have removeViewBox: false
Storybook has a different configuration, since it’s a tool for testing out components, we need to say to it what are we testing and how.
Analyzing the main.js file from storybook folder we need to understand better this section.
webpackFinal: async (config, { configType }) => {
// the @ alias points to the `src/` directory, a common alias
// used in the Vue community
config.resolve.alias["@"] = path.resolve(__dirname, "..", "src")
// THIS is the tricky stuff!
config.module.rules.push({
test: /\.scss$/,
use: [
"style-loader",
"css-loader",
{
loader: "sass-loader",
options: {
additionalData:
"@import '@/styles/variables-scss.scss';",
},
},
],
include: path.resolve(__dirname, "../"),
})
const fileLoaderRule = config.module.rules.find((rule) =>
rule.test.test(".svg")
)
fileLoaderRule.exclude = /\.svg$/
config.module.rules.push({
oneOf: [
{
test: /\.(jpg|png|svg|gif)$/,
type: "asset/inline",
resourceQuery: /url/,
},
{
test: /\.svg$/,
loader: "vue-svg-loader",
options: {
svgo: {
plugins: [
{
removeViewBox: false,
},
],
},
},
},
],
})
This section is responsible for allowing storybook to read svgs from node_modules, also it’s responsible to set the configuration for removeViewBox. Also it allows for storybook to read scss from node_modules and other files.
Last but not least, we have the Rollup configuration.
This is the most important part to understand:
// rollup.config.js
import fs from "fs"
import path from "path"
import vue from "rollup-plugin-vue"
import alias from "@rollup/plugin-alias"
import commonjs from "@rollup/plugin-commonjs"
import resolve from "@rollup/plugin-node-resolve"
import replace from "@rollup/plugin-replace"
import babel from "@rollup/plugin-babel"
import { terser } from "rollup-plugin-terser"
import minimist from "minimist"
import postcss from "rollup-plugin-postcss"
import svg from "rollup-plugin-vue-inline-svg"
This part we have the packages and libraries we are using in rollup. A basic explanation for some of those are:
- rollup-plugin-vue: This is a plugin for rollup that allows you to author Vue components in a format called Single-File Components (SFCs).
- alias: A Rollup plugin for defining aliases when bundling packages.
- plugin-node-resolve: A Rollup plugin which locates modules using the Node resolution algorithm, for using third party modules in
node_modules
. This is important to load UCLA design tokens. - rollup-plugin-vue-inline-svg: A simple plugin to import svg files as vue components. This is intended to be used with rollup-plugin-vue and is based on vue-svg-loader. That is important, so we can set configurations for svgo.
And also this:
const baseConfig = {
input: "src/entry.js",
plugins: {
preVue: [
alias({
entries: [
{
find: "@",
replacement: `${path.resolve(projectRoot, "src")}`,
},
],
}),
],
replace: {
preventAssignment: true,
"process.env.NODE_ENV": JSON.stringify("production"),
},
vue: {
template: {
isProduction: true,
},
style: {
preprocessStyles: true,
preprocessOptions: {
scss: {
data: `
@import 'src/styles/variables-scss.scss';
`,
includePaths: ["node_modules/", "src/"],
importer(path) {
return {
file: path[0] !== "~" ? path : path.slice(1),
}
},
},
},
},
},
postVue: [
svg({ svgoConfig: { plugins: [{ removeViewBox: false }] } }),
resolve({
extensions: [".js", ".jsx", ".ts", ".tsx", ".vue"],
}),
postcss({
include: /\.scss$/,
use: {
sass: {
data: `
@import 'src/styles/variables-scss.scss';
`,
},
},
}),
commonjs(),
],
babel: {
exclude: "node_modules/**",
extensions: [".js", ".jsx", ".ts", ".tsx", ".vue"],
babelHelpers: "bundled",
},
},
}
This baseConfig
has a lot of important parts. First thing we need to understand is the steps, first we have preVue, where we set the alias for the src
, so rollup can understand our usage.
The second step is replace
, where we set rollup for production.
Next is vue
, we say that’s is production, then style
is inside to configure scss, in this specific case we are importing some styles from node_modules, so we set some configuration for that. In our case we import that from src/styles/variables-scss.scss
Also, in this part we add the configuration to read scss from node_modules includePaths: ["node_modules/", "src/"],
Next we see the postVue
part, this part will be loaded after vue is loaded, that's called post vue. It seems as we are repeating the vue part.
- Don't use
lang="html"
- example:
<template lang=¨html¨> <nav :class="classes"> <div class="item-top"> ...
- example:
- Replace
nuxt-link
withrouter-link
- Place the name of the component:
name: "NavPrimary",
- Instead of using
~
to go to source, use@
.- example:
import SmartLink from "@/lib-components/SmartLink"
- example:
- We call svgs from design-tokens like this:
import SvgLogoUclaLibrary from "ucla-library-design-tokens/assets/svgs/logo-library.svg"
- Svgs are called as components.
- example
import SvgLogoUclaLibrary from "ucla-library-design-tokens/assets/svgs/logo-library.svg" export default { name: "NavPrimary", components: { SvgLogoUclaLibrary, },
- example
- In storybook we need to import the component at the top and add the component inside the
export default
and also in any other instances that we are exporting.- example:
import AlphabeticalBrowseBy from "../lib-components/AlphabeticalBrowseBy" export default { title: "SEARCH / AlphabeticalBrowseBy", component: AlphabeticalBrowseBy, } export const Default = () => ({ components: { AlphabeticalBrowseBy }, template: `<alphabetical-browse-by/>`, }) export const CIsSelected = () => ({ components: { AlphabeticalBrowseBy }, template: `<alphabetical-browse-by selectedLetterProp="C"/>`, })
- example:
- When a component is using
router-link
we need to add this to storybook also. We add by usingStoryRouter
decorator.- example:
import BannerFeatured from "@/lib-components/BannerFeatured" import HeadingArrow from "@/lib-components/HeadingArrow" import StoryRouter from "storybook-vue-router" // Import mock api data import * as API from "@/stories/mock-api.json" export default { title: "Banner Featured", component: BannerFeatured, decorators: [StoryRouter()], }
- example:
- Another thing that you might want to use is vuex store, in that case you need to add the following to storybook:
- First, import vuex:
// Storybook default settings import Vue from "vue" import Vuex from "vuex" import BlockCallToAction from "@/lib-components/BlockCallToAction" Vue.use(Vuex)
- Then make a mock of the vuex store inside the exported component in question:
export const GlobalAskALibrarian = () => ({ store: new Vuex.Store({ state: { globals: { askALibrarian: { id: "7322", askALibrarianTitle: "Have further questions?", askALibrarianText: "<p>We're here to help. Chat with a librarian 24/7, schedule a research consultation or email us your quick questions.</p>", buttonUrl: [ { buttonText: "Contact us", buttonUrl: "/help/", }, ], }, }, }, }), data() { return { ...mock, } }, components: { BlockCallToAction }, template: ` <block-call-to-action :is-global="true" /> `, })
- First, import vuex:
- Always remember to add
this
when refering to a function or mixin. - Mixins have almost the same functionality as global functions, so use it wisely and refer to them accordingly.
Vuex is a state management pattern + library for Vue.js applications. It serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion.
When dealing with store data, we need to understand some basic concepts
- The state, the source of truth that drives our app
- The view, a declarative mapping of the state
- The actions, the possible ways the state could change in reaction to user inputs from the view.
State : This is the data that will be shared in the application. so instead passing it via props. we can simply have it in our store and have our components access them directly.
Getters: According to the Vuex documentation, we think of getters as the computed property for store and it has an helper , which is the mapGetters Helper
that simply takes out store getters to out component computed property.
Mutations: State can only be changed in a vuex store by commiting a mutation. A mutation cannot be called directly. Inorder to do so, you need to use store.commit
. Instead of committing a mutation in a component methods, we simply dispatch an action on the mutation.
Actions : Action commits a mutation using the contex.comit
and dispatch the action using store.dispatch
. We also have the mapAction helpers
.
Our implementation of vuex uses modules. Due to using a single state tree, all states of our application are contained inside one big object. As our application grows in scale, the store can get really bloated.
To help with that, Vuex allows us to divide our store into **modules**. Each module can contain its own `state`, `mutations`, `actions`, `getters`, and even nested modules - it's turtles all the way down:
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> `moduleA`'s state
store.state.b // -> `moduleB`'s state
Our index inside the store folder is importing all modules:
import Vue from "vue"
import Vuex from "vuex"
import HeaderSmart from "./modules/headerSmart.js"
import FooterPrimary from "./modules/footerPrimary.js"
import FooterSock from "./modules/footerSock.js"
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
headerSmart: HeaderSmart,
footerPrimary: FooterPrimary,
footerSock: FooterSock,
},
})
Then, inside each module we see something like this:
const mock = {
socialItems: [
{
id: "11777",
name: "Twotter",
to: "https://twitter.com/",
classes: null,
target: "1",
},
],
}
export default {
state: {
header: {
primary: mock.primary,
secondary: mock.secondary,
},
nodes: [
{
children: mock.socialItems,
},
{
children: mock.pressItems,
},
],
winWidth: 824,
},
getters: {
getHeaderSmartData: (state) => state.header,
getHeaderSmartWinWidth: (state) => state.winWidth,
},
mutations: {},
actions: {},
}
To call the information we need from the state, we need to use mapGetters
.
To use it, we call mapGetters
inside the vue component, destructing it and also using it inside the computed property.
Like this:
<script>
import SiteBrandBar from "@/lib-components/SiteBrandBar"
import HeaderMainResponsive from "@/lib-components/HeaderMainResponsive"
import HeaderMain from "@/lib-components/HeaderMain"
import { mapGetters } from "vuex"
export default {
name: "HeaderSmart",
components: {
SiteBrandBar,
HeaderMainResponsive,
HeaderMain,
},
computed: {
...mapGetters(["getHeaderSmartData", "getHeaderSmartWinWidth"]),
primaryMenuItems() {
return this.getHeaderSmartData.primary
},
secondaryMenuItems() {
return this.getHeaderSmartData.secondary
},
isMobile() {
return this.getHeaderSmartWinWidth <= 1024 ? true : false
},
whichHeader() {
return this.isMobile ? "header-main-responsive" : "header-main"
},
},
}
</script>
You can see that we import mapGetters
from vuex, then we add it to computed
section and then we call every getter inside the array. To use it we just place this.<name_of_getter>
.