2018年11月18日 星期日
Implement algolia autocomplete with rails
```js
// app/javascripts/application.js
const algoliasearch = require('algoliasearch/lite')
const autocomplete = require('autocomplete.js')
const client = algoliasearch(process.env.ALGOLIA_APPLICATION_ID, process.env.ALGOLIA_SEARCH_ONLY_API_KEY)
const pack = {
algoliaClient: client,
autocomplete: autocomplete
}
window.Pack = pack
```
```erb
<!-- Algolia search autocomplete HTML markup -->
<div class="aa-input-container" id="aa-input-container">
<input type="search" id="aa-search-input" class="aa-input-search" placeholder="Search multiple fields in one place..." name="search" autocomplete="off" />
<svg class="aa-input-icon" viewBox="654 -372 1664 1664">
<path d="M1806,332c0-123.3-43.8-228.8-131.5-316.5C1586.8-72.2,1481.3-116,1358-116s-228.8,43.8-316.5,131.5 C953.8,103.2,910,208.7,910,332s43.8,228.8,131.5,316.5C1129.2,736.2,1234.7,780,1358,780s228.8-43.8,316.5-131.5 C1762.2,560.8,1806,455.3,1806,332z M2318,1164c0,34.7-12.7,64.7-38,90s-55.3,38-90,38c-36,0-66-12.7-90-38l-343-342 c-119.3,82.7-252.3,124-399,124c-95.3,0-186.5-18.5-273.5-55.5s-162-87-225-150s-113-138-150-225S654,427.3,654,332 s18.5-186.5,55.5-273.5s87-162,150-225s138-113,225-150S1262.7-372,1358-372s186.5,18.5,273.5,55.5s162,87,225,150s113,138,150,225 S2062,236.7,2062,332c0,146.7-41.3,279.7-124,399l343,343C2305.7,1098.7,2318,1128.7,2318,1164z" />
</svg>
</div>
<!-- Initialize autocomplete menu -->
<script>
var index = Pack.algoliaClient.initIndex('<%= index_name %>');
Pack.autocomplete('#aa-search-input',
{ hint: false }, {
source: Pack.autocomplete.sources.hits(index, {hitsPerPage: 5, filters: 'discarded_at_i < 1'}),
displayKey: 'name',
templates: {
suggestion: function(suggestion) {
return `<a href='<%= path_prefix %>/${suggestion.objectID}'>${suggestion._highlightResult.name.value}</a>`
}
}
}).on("autocomplete:selected", function (event, suggestion, dataset) {
window.location.href = `<%= path_prefix %>/${suggestion.objectID}`;
})
</script>
```
2018年11月17日 星期六
Submit a rails ActiveStorage issue
https://github.com/rails/rails/issues/34470
### Steps to reproduce
When passing invalid file object to initialize DirectUpload instance, it will fail silently while creating because FileChecksum fail to acknowledge it's a invalid object.
### Expected behavior
I expect when I passing a invalid file object to initialize DirectUpload instance to create upload, it should show error message.
```js
// This result is what I got from UppyJS, it doesn't contains real file data but it still provide meta data like file mimeType, name...etc
const result = {
"source": "GoogleDrive",
"id": "uppy-kittyjpg-image/jpeg",
"name": "kitty.jpg",
"extension": "jpg",
"meta": {
"name": "kitty.jpg",
"type": "image/jpeg"
},
"type": "image/jpeg",
"data": {
"isFolder": false,
"icon": "https://drive-thirdparty.googleusercontent.com/16/type/image/jpeg",
"name": "kitty.jpg",
"mimeType": "image/jpeg",
"id": "1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"thumbnail": "http://localhost:3020/drive/thumbnail/1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"requestPath": "1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"modifiedDate": "2018-11-07T15:06:35.239Z",
"custom": {
"isTeamDrive": false
}
},
"progress": {
"percentage": 0,
"bytesUploaded": 0,
"bytesTotal": 0,
"uploadComplete": false,
"uploadStarted": false
},
"size": 0,
"isRemote": true,
"remote": {
"serverUrl": "http://localhost:3020",
"url": "http://localhost:3020/drive/get/1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"body": {
"fileId": "1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU"
},
"providerOptions": {
"serverUrl": "http://localhost:3020",
"provider": "drive",
"authProvider": "google"
}
},
"preview": "http://localhost:3020/drive/thumbnail/1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU"
}
const uploadFile = (file) {
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
upload.create((error, blob) => {
if (error) {
console.log("Error!!")
} else {
console.log("No Error")
}
})
}
uploadFile(result.data) // => Should print something
```
### Actual behavior
```js
const uploadFile = (file) {
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
upload.create((error, blob) => {
if (error) {
console.log("Error!!")
} else {
console.log("No Error")
}
})
}
uploadFile(result.data) // => Nothing printed
```
### Digging into it
The reason there are no error is because `rails/activestorage/app/javascript/activestorage/file_checksum.js`, in `create` function it only add error listener for fileReader, however, when passing invalid file object into FileChecksum, in this case, in line 14, `this.file.size` is `undefined` so `this.chunkCount` will be `NaN` at the first place, it never had chance to raise error inside fileReader.
According to https://github.com/rails/rails/blob/master/activestorage/app/javascript/activestorage/file_checksum.js#L42
It will just return false and invoke no callback.
I don't know if it's designed by purpose or maybe we can add some check to make sure file object is valid? It might not be an issue if you know what you're doing very well, however, it did take me a while to investigate it while I was trying to integrate Uppy with ActiveStorage, so I think it's worth to mention or add checking mechanism into codebase. Any thought? I'm happy to send a PR for this if you think it's worth to check.
### System configuration
**Rails version**:
5.2.1
**Ruby version**:
2.5.1
**ActiveStorage npm package version**
5.2.1
### Steps to reproduce
When passing invalid file object to initialize DirectUpload instance, it will fail silently while creating because FileChecksum fail to acknowledge it's a invalid object.
### Expected behavior
I expect when I passing a invalid file object to initialize DirectUpload instance to create upload, it should show error message.
```js
// This result is what I got from UppyJS, it doesn't contains real file data but it still provide meta data like file mimeType, name...etc
const result = {
"source": "GoogleDrive",
"id": "uppy-kittyjpg-image/jpeg",
"name": "kitty.jpg",
"extension": "jpg",
"meta": {
"name": "kitty.jpg",
"type": "image/jpeg"
},
"type": "image/jpeg",
"data": {
"isFolder": false,
"icon": "https://drive-thirdparty.googleusercontent.com/16/type/image/jpeg",
"name": "kitty.jpg",
"mimeType": "image/jpeg",
"id": "1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"thumbnail": "http://localhost:3020/drive/thumbnail/1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"requestPath": "1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"modifiedDate": "2018-11-07T15:06:35.239Z",
"custom": {
"isTeamDrive": false
}
},
"progress": {
"percentage": 0,
"bytesUploaded": 0,
"bytesTotal": 0,
"uploadComplete": false,
"uploadStarted": false
},
"size": 0,
"isRemote": true,
"remote": {
"serverUrl": "http://localhost:3020",
"url": "http://localhost:3020/drive/get/1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"body": {
"fileId": "1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU"
},
"providerOptions": {
"serverUrl": "http://localhost:3020",
"provider": "drive",
"authProvider": "google"
}
},
"preview": "http://localhost:3020/drive/thumbnail/1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU"
}
const uploadFile = (file) {
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
upload.create((error, blob) => {
if (error) {
console.log("Error!!")
} else {
console.log("No Error")
}
})
}
uploadFile(result.data) // => Should print something
```
### Actual behavior
```js
const uploadFile = (file) {
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
upload.create((error, blob) => {
if (error) {
console.log("Error!!")
} else {
console.log("No Error")
}
})
}
uploadFile(result.data) // => Nothing printed
```
### Digging into it
The reason there are no error is because `rails/activestorage/app/javascript/activestorage/file_checksum.js`, in `create` function it only add error listener for fileReader, however, when passing invalid file object into FileChecksum, in this case, in line 14, `this.file.size` is `undefined` so `this.chunkCount` will be `NaN` at the first place, it never had chance to raise error inside fileReader.
According to https://github.com/rails/rails/blob/master/activestorage/app/javascript/activestorage/file_checksum.js#L42
It will just return false and invoke no callback.
I don't know if it's designed by purpose or maybe we can add some check to make sure file object is valid? It might not be an issue if you know what you're doing very well, however, it did take me a while to investigate it while I was trying to integrate Uppy with ActiveStorage, so I think it's worth to mention or add checking mechanism into codebase. Any thought? I'm happy to send a PR for this if you think it's worth to check.
### System configuration
**Rails version**:
5.2.1
**Ruby version**:
2.5.1
**ActiveStorage npm package version**
5.2.1
Integrate Uppy with Rails and ActiveStorage and use google drive direct upload
We're going to integrate Uppy with "upload from Google Drive" feature with rails backend and ActiveStorage, in this case we are going to upload multiple images at once and using the direct upload concept.
What we need?
1. Install Uppy JS into rails using webpacker
- Figure out how direct upload works with ActiveStorage
- A companion server (a proxy server to handle google auth)
## 1. Install Uppy JS into rails using webpacker
```json
// package.json
{
"name": "platform",
"private": true,
"dependencies": {
"@rails/webpacker": "3.5",
"dotenv": "^6.1.0",
"uppy": "^0.28.0"
},
"devDependencies": {
"webpack-dev-server": "2.11.2"
}
}
```
To hide credential from git history, we add `dotenv` npm package to do so
```js
const { environment } = require('@rails/webpacker')
const webpack = require('webpack')
const dotenv = require('dotenv')
const dotenvFiles = [
`.env.${process.env.NODE_ENV}.local`,
'.env.local',
`.env.${process.env.NODE_ENV}`,
'.env'
]
dotenvFiles.forEach((dotenvFile) => {
dotenv.config({ path: dotenvFile, silent: true })
})
environment.plugins.prepend('Environment', new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(process.env))))
module.exports = environment
```
```
# .env
APPLICATION_URL=http://localhost:3001
UPPY_COMPANION_SERVER_URL=http://localhost:3020
```
```js
// app/javascripts/uppy.js
const Uppy = require("@uppy/core")
const Dashboard = require("@uppy/dashboard")
const XHRUpload = require("@uppy/xhr-upload")
const GoogleDrive = require("@uppy/google-drive")
const input = document.querySelector("input[type=file]")
const form = document.querySelector("form")
const csrfToken = document.querySelector('meta[name="csrf-token"]').content
const uppyTrigger = "#js-uppy-upload"
const uppyTriggerElement = document.querySelector(uppyTrigger)
const startUppy = () => {
const uppy = Uppy({
debug: true,
restrictions: {
allowedFileTypes: ['image/*', '.jpg', '.jpeg', '.png', '.gif']
},
})
.use(Dashboard, { trigger: uppyTrigger })
.use(GoogleDrive, {
target: Dashboard,
serverUrl: process.env.UPPY_COMPANION_SERVER_URL || "http://localhost:3020"
})
.use(XHRUpload, {
endpoint: `${process.env.APPLICATION_URL}/uploader/image`,
bundle: false,
headers: { "X-CSRF-Token": csrfToken }
})
uppy.on("upload-success", (file, body) => {
uppy.setFileState(file.id, {
xhr: Object.assign({}, file, {
uploadURL: body["uploadURL"],
signedId: body["signedId"]
})
})
})
uppy.on("complete", result => {
result.successful.forEach(file => {
insertImageSignedId(form, input, file.xhr.signedId)
})
})
}
const insertImageSignedId = (form, input, signed_id) => {
const hiddenField = document.createElement("input")
hiddenField.setAttribute("type", "hidden")
hiddenField.setAttribute("value", signed_id)
hiddenField.name = input.name
form.appendChild(hiddenField)
}
if (uppyTriggerElement) {
startUppy()
}
```
And add uppy.js into layout
```erb
<!DOCTYPE html>
<html>
<head>
<title>Uppy Example</title>
<%= stylesheet_link_tag 'admin/default', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'admin', 'data-turbolinks-track': 'reload' %>
<%= csrf_meta_tags %>
</head>
<body>
<div class="container">
<%= yield %>
<%= javascript_pack_tag 'uppy' %>
</div>
</body>
</html>
```
form example
```erb
<div class="form-group">
<%= f.label "Images" %>
<div id="js-uppy-upload" class="form-control">
Upload
</div>
<%= %>
<%= f.file_field :images, multiple: true, class: "hidden" %>
</div>
```
## 2. Figure out how direct upload works with ActiveStorage
Add a new endpoint into your rails project, this simplest implementation is below:
```rb
class UploaderController < ApplicationController
# TODO: Add CSRF protection
skip_before_action :verify_authenticity_token
# TODO: Add authentication, cookie based authentication should be good enough
def image
# XHRUpload will send image 1 by 1
# https://uppy.io/docs/xhr-upload/#bundle-false
file = params[:files].first
blob = ActiveStorage::Blob.create_after_upload!(
io: file,
filename: file.original_filename,
content_type: file.content_type
)
render json: { uploadURL: url_for(blob), signedId: blob.signed_id }
end
end
```
Cons here is that we still need to process image file in our server, I can't find a way to avoid it with ActiveStorage, if you know a better way I'm happy to hear, thanks :)
Now the upload workflow will be
User select files from google drive -> Client tell companion server to upload file to our API -> API server process image, save it to AWS S3 and return ActiveStorage's record's (blob) signed_id -> Client add signed_id into form field and submit with it -> Rails server bind uploaded image with form record
## 3. A companion server (a proxy server to handle google auth)
I use the heroku node template, so there are heroku cli dependency (to load .env file), you can remove this dependency simply by loading `.env` yourself
```json
// package.json
{
"name": "example-uppy-companion-server",
"version": "1.0.0",
"description": "Example uppy companion server",
"engines": {
"node": "10.x"
},
"main": "index.js",
"scripts": {
"start": "heroku local"
},
"dependencies": {
"@uppy/companion": "^0.15.0",
"body-parser": "^1.18.3",
"express": "^4.16.4",
"express-session": "^1.15.6"
},
"devDependencies": {
"request": "^2.81.0",
"tape": "^4.7.0"
}
}
```
```js
// index.js
const express = require('express')
const companion = require('@uppy/companion')
const bodyParser = require('body-parser')
const session = require('express-session')
const app = express()
app.use(bodyParser.json())
app.use(session({
secret: 'some-secret',
resave: true,
saveUninitialized: true
}))
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*')
res.setHeader(
'Access-Control-Allow-Methods',
'GET, POST, OPTIONS, PUT, PATCH, DELETE'
)
res.setHeader(
'Access-Control-Allow-Headers',
'Authorization, Origin, Content-Type, Accept'
)
res.setHeader('Access-Control-Allow-Credentials', 'true')
next()
})
// Routes
app.get('/', (req, res) => {
res.setHeader('Content-Type', 'text/plain')
res.send('Welcome to Companion')
})
// initialize uppy
const uppyOptions = {
providerOptions: {
google: {
key: process.env.PROVIDER_GOOGLE_DRIVE_KEY || "",
secret: process.env.PROVIDER_GOOGLE_DRIVE_SECRET || 5
}
},
server: {
host: process.env.PROVIDER_SERVER_HOST || "localhost:3020",
protocol: process.env.PROVIDER_SERVER_PROTOCOL || "http"
},
filePath: "./output",
secret: "some-secret",
debug: true
};
app.use(companion.app(uppyOptions))
// handle 404
app.use((req, res, next) => {
return res.status(404).json({ message: 'Not Found' })
})
// handle server errors
app.use((err, req, res, next) => {
console.error('\x1b[31m', err.stack, '\x1b[0m')
res.status(err.status || 500).json({ message: err.message, error: err })
})
companion.socket(app.listen(process.env.PORT || 3020), uppyOptions)
console.log('Welcome to Companion!')
```
```
# .env
PROVIDER_SERVER_HOST=localhost:3020
PROVIDER_SERVER_PROTOCOL=http
PROVIDER_GOOGLE_DRIVE_KEY=PROVIDER_GOOGLE_DRIVE_KEY
PROVIDER_GOOGLE_DRIVE_SECRET=PROVIDER_GOOGLE_DRIVE_SECRET
PORT=3020
```
https://stackoverflow.com/questions/50872912/integration-rails-active-storage-with-js-uploaders
https://github.com/rails/rails/issues/32251
https://medium.com/weareevermore/manual-uploads-using-activestorage-47808dab1b65
What we need?
1. Install Uppy JS into rails using webpacker
- Figure out how direct upload works with ActiveStorage
- A companion server (a proxy server to handle google auth)
## 1. Install Uppy JS into rails using webpacker
```json
// package.json
{
"name": "platform",
"private": true,
"dependencies": {
"@rails/webpacker": "3.5",
"dotenv": "^6.1.0",
"uppy": "^0.28.0"
},
"devDependencies": {
"webpack-dev-server": "2.11.2"
}
}
```
To hide credential from git history, we add `dotenv` npm package to do so
```js
const { environment } = require('@rails/webpacker')
const webpack = require('webpack')
const dotenv = require('dotenv')
const dotenvFiles = [
`.env.${process.env.NODE_ENV}.local`,
'.env.local',
`.env.${process.env.NODE_ENV}`,
'.env'
]
dotenvFiles.forEach((dotenvFile) => {
dotenv.config({ path: dotenvFile, silent: true })
})
environment.plugins.prepend('Environment', new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(process.env))))
module.exports = environment
```
```
# .env
APPLICATION_URL=http://localhost:3001
UPPY_COMPANION_SERVER_URL=http://localhost:3020
```
```js
// app/javascripts/uppy.js
const Uppy = require("@uppy/core")
const Dashboard = require("@uppy/dashboard")
const XHRUpload = require("@uppy/xhr-upload")
const GoogleDrive = require("@uppy/google-drive")
const input = document.querySelector("input[type=file]")
const form = document.querySelector("form")
const csrfToken = document.querySelector('meta[name="csrf-token"]').content
const uppyTrigger = "#js-uppy-upload"
const uppyTriggerElement = document.querySelector(uppyTrigger)
const startUppy = () => {
const uppy = Uppy({
debug: true,
restrictions: {
allowedFileTypes: ['image/*', '.jpg', '.jpeg', '.png', '.gif']
},
})
.use(Dashboard, { trigger: uppyTrigger })
.use(GoogleDrive, {
target: Dashboard,
serverUrl: process.env.UPPY_COMPANION_SERVER_URL || "http://localhost:3020"
})
.use(XHRUpload, {
endpoint: `${process.env.APPLICATION_URL}/uploader/image`,
bundle: false,
headers: { "X-CSRF-Token": csrfToken }
})
uppy.on("upload-success", (file, body) => {
uppy.setFileState(file.id, {
xhr: Object.assign({}, file, {
uploadURL: body["uploadURL"],
signedId: body["signedId"]
})
})
})
uppy.on("complete", result => {
result.successful.forEach(file => {
insertImageSignedId(form, input, file.xhr.signedId)
})
})
}
const insertImageSignedId = (form, input, signed_id) => {
const hiddenField = document.createElement("input")
hiddenField.setAttribute("type", "hidden")
hiddenField.setAttribute("value", signed_id)
hiddenField.name = input.name
form.appendChild(hiddenField)
}
if (uppyTriggerElement) {
startUppy()
}
```
And add uppy.js into layout
```erb
<!DOCTYPE html>
<html>
<head>
<title>Uppy Example</title>
<%= stylesheet_link_tag 'admin/default', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'admin', 'data-turbolinks-track': 'reload' %>
<%= csrf_meta_tags %>
</head>
<body>
<div class="container">
<%= yield %>
<%= javascript_pack_tag 'uppy' %>
</div>
</body>
</html>
```
form example
```erb
<div class="form-group">
<%= f.label "Images" %>
<div id="js-uppy-upload" class="form-control">
Upload
</div>
<%= %>
<%= f.file_field :images, multiple: true, class: "hidden" %>
</div>
```
## 2. Figure out how direct upload works with ActiveStorage
ActuveStorage has it's own direct upload JS client, however, it's not useful in this case because it's only able to upload real file data from local but not able to handle google drive upload, therefore we need to implement our own `direct upload` API for it, thankfully it's easy
Add a new endpoint into your rails project, this simplest implementation is below:
```rb
class UploaderController < ApplicationController
# TODO: Add CSRF protection
skip_before_action :verify_authenticity_token
# TODO: Add authentication, cookie based authentication should be good enough
def image
# XHRUpload will send image 1 by 1
# https://uppy.io/docs/xhr-upload/#bundle-false
file = params[:files].first
blob = ActiveStorage::Blob.create_after_upload!(
io: file,
filename: file.original_filename,
content_type: file.content_type
)
render json: { uploadURL: url_for(blob), signedId: blob.signed_id }
end
end
```
Cons here is that we still need to process image file in our server, I can't find a way to avoid it with ActiveStorage, if you know a better way I'm happy to hear, thanks :)
Now the upload workflow will be
User select files from google drive -> Client tell companion server to upload file to our API -> API server process image, save it to AWS S3 and return ActiveStorage's record's (blob) signed_id -> Client add signed_id into form field and submit with it -> Rails server bind uploaded image with form record
## 3. A companion server (a proxy server to handle google auth)
I use the heroku node template, so there are heroku cli dependency (to load .env file), you can remove this dependency simply by loading `.env` yourself
```json
// package.json
{
"name": "example-uppy-companion-server",
"version": "1.0.0",
"description": "Example uppy companion server",
"engines": {
"node": "10.x"
},
"main": "index.js",
"scripts": {
"start": "heroku local"
},
"dependencies": {
"@uppy/companion": "^0.15.0",
"body-parser": "^1.18.3",
"express": "^4.16.4",
"express-session": "^1.15.6"
},
"devDependencies": {
"request": "^2.81.0",
"tape": "^4.7.0"
}
}
```
```js
// index.js
const express = require('express')
const companion = require('@uppy/companion')
const bodyParser = require('body-parser')
const session = require('express-session')
const app = express()
app.use(bodyParser.json())
app.use(session({
secret: 'some-secret',
resave: true,
saveUninitialized: true
}))
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*')
res.setHeader(
'Access-Control-Allow-Methods',
'GET, POST, OPTIONS, PUT, PATCH, DELETE'
)
res.setHeader(
'Access-Control-Allow-Headers',
'Authorization, Origin, Content-Type, Accept'
)
res.setHeader('Access-Control-Allow-Credentials', 'true')
next()
})
// Routes
app.get('/', (req, res) => {
res.setHeader('Content-Type', 'text/plain')
res.send('Welcome to Companion')
})
// initialize uppy
const uppyOptions = {
providerOptions: {
google: {
key: process.env.PROVIDER_GOOGLE_DRIVE_KEY || "",
secret: process.env.PROVIDER_GOOGLE_DRIVE_SECRET || 5
}
},
server: {
host: process.env.PROVIDER_SERVER_HOST || "localhost:3020",
protocol: process.env.PROVIDER_SERVER_PROTOCOL || "http"
},
filePath: "./output",
secret: "some-secret",
debug: true
};
app.use(companion.app(uppyOptions))
// handle 404
app.use((req, res, next) => {
return res.status(404).json({ message: 'Not Found' })
})
// handle server errors
app.use((err, req, res, next) => {
console.error('\x1b[31m', err.stack, '\x1b[0m')
res.status(err.status || 500).json({ message: err.message, error: err })
})
companion.socket(app.listen(process.env.PORT || 3020), uppyOptions)
console.log('Welcome to Companion!')
```
```
# .env
PROVIDER_SERVER_HOST=localhost:3020
PROVIDER_SERVER_PROTOCOL=http
PROVIDER_GOOGLE_DRIVE_KEY=PROVIDER_GOOGLE_DRIVE_KEY
PROVIDER_GOOGLE_DRIVE_SECRET=PROVIDER_GOOGLE_DRIVE_SECRET
PORT=3020
```
https://stackoverflow.com/questions/50872912/integration-rails-active-storage-with-js-uploaders
https://github.com/rails/rails/issues/32251
https://medium.com/weareevermore/manual-uploads-using-activestorage-47808dab1b65
2018年11月14日 星期三
Using marked.js with hightlight.js in rails to provide github flavored markdown to github style html
In this example, I use webpacker to import highlight js and marked js and css file, you can use cdn as well.
`app/javascripts/application.js`
```js
import marked from 'marked';
import hljs from 'highlight.js';
marked.setOptions({
highlight: function(code) {
return hljs.highlightAuto(code).value;
},
})
window.marked = marked
```
Let's say we have Post model with `content` field with markdown text
`app/views/posts/show.html.erb`
```erb
<h1><%= @post.title %></h1>
<%= @post.user.email %> - <%= @post.created_at %>
<div id="postContent">
<%= @post.content %>
</div>
<%= content_for :javascripts do %>
<script>
var contentElement = document.querySelector('#postContent')
contentElement.innerHTML = marked(contentElement.innerHTML)
</script>
<% end %>
```
And add application.js into layout by adding this line into `app/views/layout/application.html.erb`
```erb
<%= javascript_pack_tag 'application' %>
```
Now marked.js and hightligh.js are both working properly, check the HTML DOM on post page and you'll see html elements with some auto generated css classes.
However, it's not showing the right styles because we only apply JS but not CSS, now what we need to do is add github style which is provided by highlight.js.
In my other post I've shown how to add it: https://everyday1percent.blogspot.com/2018/11/import-css-from-npm-module-in-rails.html
In `app/javascripts/styles.js`
```js
import 'highlight.js/styles/github.css';
```
And then add to layout
```erb
<%= stylesheet_pack_tag 'styles' %>
```
And that's it, now the Post show page will show markdown text in HTML format with github styles.
Cheers.
https://github.com/highlightjs/highlight.js/issues/578
https://marked.js.org/#/USING_ADVANCED.md#highlight
https://everyday1percent.blogspot.com/2018/11/import-css-from-npm-module-in-rails.html
標籤:
CSS,
JavaScript,
ROR
Import css from npm module in rails with webpacker
I use highlight.js
In `app/javascripts/styles.js`
```js
import 'highlight.js/styles/github.css';
```
And then add to layout
```erb
<%= stylesheet_pack_tag 'styles' %>
```
Done.
You can simply replace highlight.js with any other module like bootstrap though
https://github.com/rails/webpacker/blob/master/docs/css.md
標籤:
CSS,
JavaScript,
ROR
2018年5月24日 星期四
Vue computed v.s. methods
computed 會 cached methods 不會
computed 回傳一個值,methods 不一定
https://cn.vuejs.org/v2/guide/computed.html
2018年5月21日 星期一
deploy vue to github page
兩個不錯的方式:
1. 用 push-dir + webpack 做
https://medium.com/@codetheorist/vue-up-your-github-pages-the-right-way-955486220418
2. 用直接寫好的 plugin
https://github.com/KieferSivitz/vue-gh-pages
目前是用後者
vue-cli v3.0.0.beta10 + vue-gh-pages v1.0.1
是很方便但是問題是
1. 必須用 /docs folder 當 gh-pages 的 root
2. 如果沒有綁 domain 的話會讀不到 js, css files,因為會連到 username.github.io/js/xxx.js 而不是 username.github.io/project_name/js/xxx.js,但後者才是正確的連結
3. docs/ folder 會一直搞爛 git 很煩
更新:找到用 vue-gh-pages 但不使用 master docs/ folder 的方法了,把 deploy script 改成下面這樣就行了:
"deploy": "node ./node_modules/vue-gh-pages/index.js -b gh-pages -o ./"
目前是就乾脆買個 domain,但比較 prefer push-dir + webpack 就是了,只是比較麻煩懶得嘗試。
1. 用 push-dir + webpack 做
https://medium.com/@codetheorist/vue-up-your-github-pages-the-right-way-955486220418
2. 用直接寫好的 plugin
https://github.com/KieferSivitz/vue-gh-pages
目前是用後者
vue-cli v3.0.0.beta10 + vue-gh-pages v1.0.1
是很方便但是問題是
1. 必須用 /docs folder 當 gh-pages 的 root
2. 如果沒有綁 domain 的話會讀不到 js, css files,因為會連到 username.github.io/js/xxx.js 而不是 username.github.io/project_name/js/xxx.js,但後者才是正確的連結
3. docs/ folder 會一直搞爛 git 很煩
更新:找到用 vue-gh-pages 但不使用 master docs/ folder 的方法了,把 deploy script 改成下面這樣就行了:
"deploy": "node ./node_modules/vue-gh-pages/index.js -b gh-pages -o ./"
目前是就乾脆買個 domain,但比較 prefer push-dir + webpack 就是了,只是比較麻煩懶得嘗試。
標籤:
Deployment,
JavaScript,
Web
2017年10月17日 星期二
在網站上加上音效
原來是這樣加的XD
```js
var sound = document.getElementById("msgSound");
if (sound) {
sound.play();
}
```
```js
var sound = document.getElementById("msgSound");
if (sound) {
sound.play();
}
```
2017年10月6日 星期五
2017年4月24日 星期一
React native layout with flexbox
flex 基本上就是填充
https://facebook.github.io/react-native/docs/height-and-width.html#content
flexDirection 就是主要的方向,如果是橫向就是 row 縱向就是 column, default 是 column
justifyContent 就是根據主要方向(flexDirection)分佈元件的方式,Available options are flex-start, center, flex-end, space-around, and space-between.
space-between:在元件之間安插一樣長的空白區塊
space-around:在元件旁邊安插一樣長的空白區塊,跟 space-between 不同的是 space-around 即使是最外圍的元件的外側也會安插空白區塊
flex-start:放在 flex direction 的前端
flex-end:放在 flex direction 的後端
center:放在 flex direction 的中間
alignItems 則是根據次要方向(flexDirection 的相反)排列元件的方法,Available options are flex-start, center, flex-end, and stretch.
flex-start, center, flex-end 就不用多做解釋了,如果 flexDirection 是 column 而 alignItems 是 center 代表水平置中,flex-end 代表靠右
stretch 則代表延展,如果元件本身沒有設定次要方向的長度的話就把次要方向延展至底,例如 flexDirection 是 column 的情況下不設元件的 width 就會將元件平行拉長到底
https://facebook.github.io/react-native/docs/flexbox.html#content
標籤:
JavaScript,
React Native
2017年4月12日 星期三
React stateless component
幾個重點
1. 可以不用使用 ES6 的 class
2. State 不存在 presentational component 裡面,變成 pure function,沒有 react lifecycle
3. 沒有 react lifecycle 代表不能快速 hack 功能進去,但相對的可以維持 code 的品質
4. pure function 好測試,而且 react team 考慮(或是已經?)對這類沒有 life cycles 的 function 做效能優化
5. 不用再寫 bind(this) 這種 code
https://hackernoon.com/react-stateless-functional-components-nine-wins-you-might-have-overlooked-997b0d933dbc
標籤:
JavaScript,
ReactJS
訂閱:
文章 (Atom)