Setting an object's field


#1

How is the property of a typed js object set ?
To call a method for a js object I do this:

[@bs.send] external fillRect : (context, float, float, float, float) => unit = “”;

However to set a field I tried

let canctx : context = getContext(pagecanvas,“2d”);
canctx##fillStyle #= “green”;

Js.Dict.set(canctx,“fillStyle”,“green”);

[@bs.set] external fillStyle : (context,string) = “”;

All of them failed. To be fair I am a bit confused on how to use the interop.
So how do I set the fillStyle property of a canvas 2d context ?


#2

Given this working code:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>canvas test</title>
    <meta http-equiv="Content-Type" content="text/html"; charset="utf-8" />
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script type="text/javascript">
     const canvas = document.getElementById('canvas');
     const ctx = canvas.getContext('2d');
     ctx.fillStyle = 'green';
     ctx.fillRect(20, 10, 150, 100);
    </script>
  </body>
</html>

We can write

/* file: Demo.re */
type document;
type canvas;
type context = {. [@bs.set] "fillStyle": string};

/* https://bucklescript.github.io/docs/en/object-2 */

[@bs.val] external document: document = "";

[@bs.send] [@bs.return null_to_opt]
external getElementById: (document, string) => option(canvas) = "";

[@bs.send] [@bs.return null_to_opt]
external getContext: (canvas, string) => option(context) = "";

[@bs.send] external fillRect: (context, int, int, int, int) => unit = "";

let setupContext = context => {
  context##fillStyle #= "green";
  fillRect(context, 20, 10, 150, 100);
};

let setupCanvas = canvas =>
  switch (getContext(canvas, "2d")) {
  | Some(context) => setupContext(context)
  | None => Js.log("null context")
  };

switch (getElementById(document, "canvas")) {
| Some(canvas) => setupCanvas(canvas)
| None => Js.log("null canvas")
};
/* file: Demo.rei */
/*
   Empty interface file to avoid "exports" being generated
   in Demo.bs.js
   (at which point we need to get a bundler involved)
 */

So that this works

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>canvas test</title>
    <meta http-equiv="Content-Type" content="text/html"; charset="utf-8" />
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script type="text/javascript" src="Demo.bs.js"></script>
  </body>
</html>

Alternately this works as well:

type document;
type canvas;
type context;

[@bs.val] external document: document = "";

[@bs.send] [@bs.return null_to_opt]
external getElementById: (document, string) => option(canvas) = "";

[@bs.send] [@bs.return null_to_opt]
external getContext: (canvas, string) => option(context) = "";

/* https://bucklescript.github.io/bucklescript/Manual.html#_binding_to_getter_setter_code_bs_get_code_code_bs_set_code */

[@bs.set] external fillStyleSet: (context, string) => unit = "fillStyle";
[@bs.send] external fillRect: (context, int, int, int, int) => unit = "";

let setupContext = context => {
  fillStyleSet(context, "green");
  fillRect(context, 20, 10, 150, 100);
};

let setupCanvas = canvas =>
  switch (getContext(canvas, "2d")) {
  | Some(context) => setupContext(context)
  | None => Js.log("null context")
  };

switch (getElementById(document, "canvas")) {
| Some(canvas) => setupCanvas(canvas)
| None => Js.log("null canvas")
};

And yet another option

type document;
type canvas;

[@bs.deriving abstract]
type context = {mutable fillStyle: string};

/* https://bucklescript.github.io/docs/en/object#write */

[@bs.val] external document: document = "";

[@bs.send] [@bs.return null_to_opt]
external getElementById: (document, string) => option(canvas) = "";

[@bs.send] [@bs.return null_to_opt]
external getContext: (canvas, string) => option(context) = "";

[@bs.send] external fillRect: (context, int, int, int, int) => unit = "";

let setupContext = context => {
  fillStyleSet(context, "green");
  fillRect(context, 20, 10, 150, 100);
};

let setupCanvas = canvas =>
  switch (getContext(canvas, "2d")) {
  | Some(context) => setupContext(context)
  | None => Js.log("null context")
  };

switch (getElementById(document, "canvas")) {
| Some(canvas) => setupCanvas(canvas)
| None => Js.log("null canvas")
};

#3

Thanks for the reply.
I tried out the third method and it worked out reasonably well. However I have a follow up question. How do I attach an event to this object. Can I directly pass reasonML functions or do I have to use bs.raw.


#4
/*
   file: Demo.re
   Original: https://github.com/curran/HTML5Examples/blob/gh-pages/canvas/mouseFollower/script.js

   Note: Demo.bs.js needs to be bundled to include the "bs-platform/lib/js/curry.js" dependency.

   For example, generate ./dist/index.html (and serve at http://localhost:1234/ ) :
   $ npm i parcel-bundler -D
   $ ./node_modules/.bin/parcel ./src/index.html

 */

module Event = {
  type t;
  [@bs.get] external clientXGet: t => float = "clientX";
  [@bs.get] external clientYGet: t => float = "clientY";
  /* see also Dom.event https://bucklescript.github.io/bucklescript/api/Dom.html#TYPEevent
       in https://github.com/BuckleScript/bucklescript/blob/master/jscomp/others/dom.mli
       and https://github.com/reasonml-community/bs-webapi-incubator/issues/63
     */
};

module Context = {
  type t;
  [@bs.set] external fillStyleSet: (t, string) => unit = "fillStyle";
  [@bs.set] external lineWidthSet: (t, float) => unit = "lineWidth";
  [@bs.set] external strokeStyleSet: (t, string) => unit = "strokeStyle";
  [@bs.send]
  external arc: (t, float, float, float, float, float, bool) => unit = "";
  [@bs.send] external beginPath: t => unit = "";
  [@bs.send] external clearRect: (t, float, float, float, float) => unit = "";
  [@bs.send] external fill: t => unit = "";
  [@bs.send] external stroke: t => unit = "";
};

module Canvas = {
  type t;

  [@bs.get] external heightGet: t => float = "height";
  [@bs.get] external widthGet: t => float = "width";
  [@bs.send]
  external addEventListener: (t, string, Event.t => unit) => unit = "";
  [@bs.send] [@bs.return null_to_opt]
  external getContext: (t, string) => option(Context.t) = "";
};

type document;

[@bs.val] external document: document = "";

[@bs.send] [@bs.return null_to_opt]
external getElementById: (document, string) => option(Canvas.t) = "";

type circle = {
  x: float,
  y: float,
  radius: float,
};

let makeCircle = (x, y, radius) => {x, y, radius};
let moveCircle = (circle, x, y) => {...circle, x, y};

let drawCircle = (context, clearCanvas, {radius, x, y}) => {
  /* Clear the background */
  clearCanvas();

  /* Establish the circle path */
  Context.beginPath(context);
  Context.arc(context, x, y, radius, 0.0, Js.Math._PI *. 2.0, false);

  /* Fill the circle */
  Context.fillStyleSet(context, "00F0FF");
  Context.fill(context);

  /* Outline (stroke) the circle */
  Context.lineWidthSet(context, 4.0);
  Context.strokeStyleSet(context, "black");
  Context.stroke(context);
};

let setupContext = (canvas, context) => {
  let x = Canvas.widthGet(canvas) /. 2.0;
  let y = Canvas.heightGet(canvas) /. 2.0;
  let circle = ref(makeCircle(x, y, 30.0));
  /* https://reasonml.github.io/docs/en/mutation */

  let clearCanvas = () => {
    let width = Canvas.widthGet(canvas);
    let height = Canvas.heightGet(canvas);
    Context.clearRect(context, 0.0, 0.0, width, height);
  };

  let draw = event => {
    let x = Event.clientXGet(event);
    let y = Event.clientYGet(event);
    circle := moveCircle(circle^, x, y);
    drawCircle(context, clearCanvas, circle^);
  };

  /* Redraw the circle every time the mouse moves */
  Canvas.addEventListener(canvas, "mousemove", draw);

  /* Clear the canvas when the mouse leaves the canvas region */
  Canvas.addEventListener(canvas, "mouseout", _event => clearCanvas());

  drawCircle(context, clearCanvas, circle^);
};

let setupCanvas = canvas =>
  switch (Canvas.getContext(canvas, "2d")) {
  | Some(context) => setupContext(canvas, context)
  | None => Js.log("null context")
  };

switch (getElementById(document, "canvas")) {
| Some(canvas) => setupCanvas(canvas)
| None => Js.log("null canvas")
};