1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

nimでカスタムエレメントを作れるかがんばってみた、疲れた

Last updated at Posted at 2022-03-21

まず、1.5までなんだって

nim の javascriptの対応バージョンは、1.5まででそれ以降は対応予定はないそうです。
なのでES6で登場したclass構文には、対応することはないようです。

prototype

まずは、nimでjavascriptのprototypeでのオブジェクトの作成の方法を紹介します。

javascriptで書くと以下のものです。

function testObj(){}
testObj.prototype.say = function(){alert("I say ok?")}
var o = new testObj()
o.say()

で……これをnimで記述するのに悩みました。
クラスじゃん? type xxx = object of JsObject とかで書くと思ったわけですよ。
で、うまくいかず悩んでいたら、ふと、functionなんだから、procでいけるんじゃね?
と思ったらうまくいきました、という話です。

さて、nimに上のコードを置き換えたものが下記のものです。

import jsffi

let win {.importjs: "window".}: JsObject

proc defProtoType(
  obj: auto,
  methodName: cstring,
  functionBody: auto) {.importjs: "#.prototype[#] = #".}

proc testObj() = discard

defProtoType(testObj, "say", proc() = win.alert("I say OK ?"))

var o = jsNew(testObj)

o.say()

いやあ、importjs 便利すぎです。

{.importjs: "#.prototype[#] = #".}

こんな、= で結んだ構文にまで利用可能とは驚きでした。

ここも悩んだ末、たどり着きました。

aaa.prototype.bbb = function(){}

これのaaaはprocの名前わたすからいけるけれど、bbbの部分はどうやって渡すのよ? で、javascriptのこれって、["bbb"]と同じだって思い出したわけです。それでbbbの部分には文字列を渡すことにしました。

↓ここね。

proc defProtoType(
  obj: auto,
  methodName: cstring,
  functionBody: auto) {.importjs: "#.prototype[#] = #".}

あとは、特に説明はいらないはず!

では、次にカスタムエレメント!

カスタムエレメント - custome element

よくあるサンプルは下のようなものでしょうか。

class testTag extends HTMLElement{
  constructor(){
    super() // カスタムエレメントでは必須!
  }
  connectedCallback(){
    this.textContent = "Hello, test tag"
  }
}
customElements.define("test-tag", testTag)

これをhtmlで、下のようにかけば、「Hello, test tag」と表示されるという仕組みですね。

<test-tag></test-tag>

でも、nimでclass構文に置換する方法は……emitプラグマを使えば可能ですが、まあ、それはやりたくない。

javascriptですから、class構文ではなく、prototypeで作る方法があるのでは? と調査した結果、下記の方法でいけることがわかりました。

function testTag(){
  // ↓これは class構文のコンストラクタ内のsuper()と同じもの
  return  Reflect.construct(HTMLElement, [], new.target)
}
Object.setPrototypeOf(testTag.prototype, HTMLElement.prototype)
testTag.prototype.connectedCallback = 
  function(){ this.textContent = "Hello, test tag"}
customElements.define("test-tag", testTag)

そう、これを、上のprototypeで行った方法でnimに置き換えてあげればいい! その結果が下記のコードです。

import jsffi

let win {.importjs: "window".}:JsObject
let HTMLElement {.importjs: "window.HTMLElement".}: JsObject

proc defProtoType(obj: auto, methodName: cstring,
    function: auto) {.importjs: "#.prototype[#] = #".}

proc setPrototypeOf(objchild, objParent: auto) 
  {.importjs: "Object.setPrototypeOf(#.prototype, #.prototype)".}

proc jsSuper(objParent: JsObject): JsObject 
  {.importjs: "window.Reflect.construct(#, [], new.target)".}

proc setThisProp(key, val: cstring) {.importjs: "this[#] = #".}

proc getThisProp(key: cstring): cstring {.importjs: "this[#]".}

proc testTag():JsObject = 
  jsSuper(HTMLElement)

defProtoType(testTag, "connectedCallback", proc() =
  setThisProp("textContent", "Hello, test tag"))

setPrototypeOf(testTag, HTMLElement)

win.customElements.define("test-tag", testTag)

とりあえず、thisの呼び方がわからないので、setThisPropとかしてます。

こんなところでしょうか。

かなり基本的な部分ですが、ここからは、さほど難しくない…と思いますが、プロパティとかの実現方法は調査中です。わかったら追記しますね。

追記 Attribute

<test-tag name="smith" age="22"></test-tag>

これで、nameとageをとってきてなにかしようってやつ。

Javascript の class構文だとこんな風でしょうか。

class testTag extends HTMLElement {
  constructor(){
    super()
    this.myname = null
  }

  static get observedAttributes() {
    return ["name", "age"];
  }
  
  attributeChangedCallback(name, oldValue, newValue) {
    this[name] = newValue //連想配列 name変数には"name"やら"age"やら
    this.connectedCallback()
  }

  connectedCallback(){
    this.textContent = "Hello " + this["name"] + " " + this["age"]
  }
}

customElements.define("test-tag", testTag)

これで「Hello smith 22」と表示されるはず。

Prototype風だと下記のような感じでしょうか。

function testTag(){
  return  Reflect.construct(HTMLElement, [], new.target)
}
Object.setPrototypeOf(testTag.prototype, HTMLElement.prototype)
testTag.prototype.connectedCallback = function(){
  this.textContent = "Hello " + this["name"] + " " + this["age"]
}

testTag["observedAttributes"] =  ["name", "age"]

testTag.prototype.attributeChangedCallback = 
  function(name, oldValue, newValue){
    this[name] = newValue
    this.connectedCallback()
  }

customElements.define("test-tag", testTag)

最後にnimで。

import jsffi

let win {.importjs: "window".}: JsObject
let HTMLElement {.importjs: "window.HTMLElement".}: JsObject

proc defProtoType(obj: auto, methodName: cstring,
    function: auto) {.importjs: "#.prototype[#] = #".}

proc setProp(obj: auto, key, val: cstring) {.importjs: "#[#] = #".}
proc setProp(obj: auto, key: cstring,
             val: seq[cstring]) {.importjs: "#[#] = #".}

proc setPrototypeOf(objchild, objParent: auto)
  {.importjs: "Object.setPrototypeOf(#.prototype, #.prototype)".}

proc jsSuper(objParent: JsObject): JsObject
  {.importjs: "window.Reflect.construct(#, [], new.target)".}

proc setThisProp(key, val: cstring) {.importjs: "this[#] = #".}

proc getThisProp(key: cstring): cstring {.importjs: "this[#]".}

proc callThis(callKey: cstring) {.importjs: "this[#]()".}

proc testTag(): JsObject =
  jsSuper(HTMLElement)

setPrototypeOf(testTag, HTMLElement)

defProtoType(testTag, "connectedCallback", proc() =
  setThisProp("textContent", "Hello, test tag " &
    getThisProp("name") & " " &
    getThisProp("age")))

defProtoType(testTag, "attributeChangedCallback",
             proc(name, oldValue, newValue: cstring) =
  setThisProp(name, newValue)
  callThis("connectedCallback")
)

setProp(testTag, "observedAttributes", @[cstring"name", cstring"age"])

win.customElements.define("test-tag", testTag)

うまくいったでしょうか?

ここのサンプルはgithubにあげました。

無駄にURL長い……

おまけ

標準の htmlgenのコードをみると……

macro abbr*(e: varargs[untyped]): untyped =
  ## Generates the HTML `abbr` element.
  result = xmlCheckedTag(e, "abbr", commonAttr)

とあります。これを真似れば独自タグをhtmlgenと同じように扱えるじゃありませんか。xmlCheckedTagには、*がついていますし。

macro testTag*(e: varargs[untyped]): untyped =
  result = xmlCheckedTag(e, "test-tag", commonAttr)

これで、

body(testTag("おはー"), p("おはじゃねーよ"))

とか書くことが可能になります。追加のattributeは、aタグを参考にしましょう。

macro a*(e: varargs[untyped]): untyped =
  ## Generates the HTML `a` element.
  result = xmlCheckedTag(e, "a", "href target download rel hreflang type " &
    commonAttr)

え? karax ? karaxは今勉強中です。

参考サイト

簡単なカスタムエレメントの説明が書かれている。

実際に使うのは、下のnesなのですが、nesからlitzをimport しているため注意が必要。
また、下記の ast_pattern_matching を利用しているのでインストールが必要。ASTをパターン検索してくれるので、macro作るならあると便利そう。

ちなみに、nesでは、emit使っています。

{.emit: """
class testTag extends HTMLElement {...}
""".}

みたいなことをしています。

「Karax + Litz = Web Components in Nim」とあるとおり、litzとnesとkaraxの簡単なサンプル。

classを使わないで、prototypeのみで、カスタムエレメントを作る方法についてのgithubのissue。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?