顯示具有 JavaScript 標籤的文章。 顯示所有文章
顯示具有 JavaScript 標籤的文章。 顯示所有文章

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

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

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

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

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 就是了,只是比較麻煩懶得嘗試。

2017年10月17日 星期二

在網站上加上音效

原來是這樣加的XD

```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

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