You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@whimsical.apache.org by ru...@apache.org on 2017/09/17 13:40:07 UTC

[whimsy] branch agenda_on_vue updated: First pass conversion React.js => Vue.js

This is an automated email from the ASF dual-hosted git repository.

rubys pushed a commit to branch agenda_on_vue
in repository https://gitbox.apache.org/repos/asf/whimsy.git


The following commit(s) were added to refs/heads/agenda_on_vue by this push:
     new 83afcf2  First pass conversion React.js => Vue.js
83afcf2 is described below

commit 83afcf2b1e29e55a7b03334d4110aa1c9fade62e
Author: Sam Ruby <ru...@intertwingly.net>
AuthorDate: Sun Sep 17 09:39:22 2017 -0400

    First pass conversion React.js => Vue.js
    
    Note: Tests are NOT passing at this time, and won't be for a while
---
 www/board/agenda/Gemfile                           |  2 +-
 www/board/agenda/README.md                         | 54 +++++++++----------
 www/board/agenda/main.rb                           |  2 +-
 www/board/agenda/package.json                      |  3 +-
 www/board/agenda/views/app.js.rb                   |  3 ++
 www/board/agenda/views/buttons/add-comment.js.rb   |  2 +-
 www/board/agenda/views/buttons/add-minutes.js.rb   |  2 +-
 www/board/agenda/views/buttons/approve.js.rb       |  2 +-
 www/board/agenda/views/buttons/attend.js.rb        |  2 +-
 www/board/agenda/views/buttons/commit.js.rb        |  2 +-
 www/board/agenda/views/buttons/draft-minutes.js.rb |  2 +-
 www/board/agenda/views/buttons/email.js.rb         |  2 +-
 www/board/agenda/views/buttons/markseen.js.rb      |  2 +-
 www/board/agenda/views/buttons/message.js.rb       |  2 +-
 www/board/agenda/views/buttons/post-actions.js.rb  |  2 +-
 www/board/agenda/views/buttons/post.js.rb          | 11 ++--
 .../agenda/views/buttons/publish-minutes.js.rb     |  2 +-
 www/board/agenda/views/buttons/refresh.js.rb       |  2 +-
 www/board/agenda/views/buttons/reminders.js.rb     |  4 +-
 www/board/agenda/views/buttons/showseen.js.rb      |  2 +-
 www/board/agenda/views/buttons/timestamp.js.rb     |  2 +-
 www/board/agenda/views/buttons/vote.js.rb          |  2 +-
 .../agenda/views/elements/additional-info.js.rb    |  2 +-
 www/board/agenda/views/elements/info.js.rb         |  2 +-
 www/board/agenda/views/elements/link.js.rb         | 37 ++++++-------
 www/board/agenda/views/elements/modal-dialog.js.rb | 63 ++++++++++------------
 www/board/agenda/views/elements/pns.rb             |  2 +-
 www/board/agenda/views/elements/text.js.rb         | 16 +++---
 www/board/agenda/views/keyboard.js.rb              |  8 +--
 www/board/agenda/views/layout/footer.js.rb         |  6 +--
 www/board/agenda/views/layout/header.js.rb         |  2 +-
 www/board/agenda/views/layout/main.js.rb           | 24 +++++----
 www/board/agenda/views/pages/action-items.js.rb    | 11 ++--
 www/board/agenda/views/pages/adjournment.js.rb     |  8 +--
 www/board/agenda/views/pages/backchannel.js.rb     |  2 +-
 www/board/agenda/views/pages/bootstrap.js.rb       |  2 +-
 www/board/agenda/views/pages/cache.js.rb           |  8 +--
 www/board/agenda/views/pages/comments.js.rb        |  2 +-
 www/board/agenda/views/pages/flagged.js.rb         |  2 +-
 www/board/agenda/views/pages/fy22.js.rb            |  2 +-
 www/board/agenda/views/pages/help.js.rb            |  2 +-
 www/board/agenda/views/pages/index.js.rb           |  2 +-
 www/board/agenda/views/pages/missing.js.rb         |  2 +-
 www/board/agenda/views/pages/queue.js.rb           |  2 +-
 www/board/agenda/views/pages/report.js.rb          |  9 +---
 www/board/agenda/views/pages/roll-call.js.rb       |  4 +-
 www/board/agenda/views/pages/search.js.rb          |  2 +-
 www/board/agenda/views/pages/select-actions.rb     |  4 +-
 www/board/agenda/views/pages/shepherd.js.rb        |  2 +-
 49 files changed, 159 insertions(+), 178 deletions(-)

diff --git a/www/board/agenda/Gemfile b/www/board/agenda/Gemfile
index 26031e3..f5a468d 100644
--- a/www/board/agenda/Gemfile
+++ b/www/board/agenda/Gemfile
@@ -13,7 +13,7 @@ end
 
 gem 'rake'
 gem 'wunderbar'
-gem 'ruby2js', '>= 2.0.17'
+gem 'ruby2js'
 gem 'sinatra', '~> 2.0'
 gem 'nokogumbo'
 gem 'execjs', ('<2.5.1' if RUBY_VERSION =~ /^1/)
diff --git a/www/board/agenda/README.md b/www/board/agenda/README.md
index 344bbf3..3325165 100644
--- a/www/board/agenda/README.md
+++ b/www/board/agenda/README.md
@@ -118,17 +118,15 @@ At this point, you have something up and running.  Let's take a look around.
        stylesheet from [bootstrap](http://getbootstrap.com/).
 
      * a `<div>` element with an id of `main` followed by the HTML used
-       to present the first page fetched from the server.  If you want to
-       see a different page, go to that page and hit refresh then view
-       source again.  This content is nicely indented and other than
-       an abundance of `data-reactid` attributes that React uses to keep
-       track of things, it is fairly straightforward.
+       to present the first page fetched from the server.  If you want to see
+       a different page, go to that page and hit refresh then view source
+       again.  This content is nicely indented and is fairly straightforward.
 
-    * a few `<script>` elements that pull in react, jquery, bootstrap, and
+    * a few `<script>` elements that pull in vue, jquery, bootstrap, and
       the agenda app itself.  I suggest that you leave that for the moment,
       we'll come back to it.
 
-    * an inline script that calls `React.render` with a datastructure
+    * an inline script that calls `new Vue` with a datastructure
       containing all the data the app needs on the client to do navigation.
       Most importantly, this page contains a parsed agenda.   Mentally file
       that away for later consideration.
@@ -172,23 +170,19 @@ Viewing Source (this time, Actual Code)
 
  * the [views/pages/search.js.rb](views/pages/search.js.rb) file contains the
    code for the search page.  There are more methods defined here.  You will
-   find definitions for these methods in the React 
-   [Lifecycle Methods](http://facebook.github.io/react/docs/component-specs.html#lifecycle-methods).
-   You will see logic mixed with presentation.  React is deadly serious when
-   it adopted the slogan "rethink best practices".  What makes this work
-   is the component lifecycle that React provides.  Components have mutable
+   find definitions for these methods in the Vue 
+   [Lifecycle Methods](https://vuejs.org/v2/guide/instance.html#Lifecycle-Diagram).
+   You will see logic mixed with presentation.  What makes this work
+   is the component lifecycle that Vue provides.  Components have mutable
    state (which are the variables which are preceded by an `@` sign), and are
    passed immutable properties (variables preceded by two `@` signs).  Some
    methods are prohibited from mutating state (most notably: the `render`
-   method).  And one method (`componentWillReceiveProps`) even has access
-   to the before and after values for properties.  Don't get hung up on the
-   logic here, but do go to the navigation bar on the top right of the
-   browser page, and select `Search` and play with search live.
-
-   Two items of special note.  `dangerouslySetInnerHTML` is React's
-   "don't blame me if things go wrong" way of allowing you to add text
-   that you have properly escaped into the content of an element.  Also,
-   we are directly making use of the browser APIs for updating the
+   method).  Don't get hung up on the logic here, but do go to the navigation
+   bar on the top right of the browser page, and select `Search` and play with
+   search live.
+
+   An item of special note: we are directly making use of the browser APIs for
+   updating the
    [history](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history)
    of the window.
  
@@ -232,7 +226,7 @@ Viewing Source (this time, Actual Code)
  * I mentioned previously that element names that start with a capital
    letter are effectively macros.  You've seen `Index`, `Search`, and
    `AddComment` classes, each of which start with a capital letter.  These
-   actually are examples of what React calls components that I have described
+   actually are examples of what Vue calls components that I have described
    as acting like macros.  `views/main.html.rb' contains the 'top'.
    [views/app.js.rb](views/app.js.rb) lists all of the files that make up the
    client side of the application.
@@ -243,7 +237,7 @@ Viewing Source (this time, Actual Code)
    from the js.rb files mentioned above.  Undoubtedly you have seen small
    amounts of JavaScript before but I suspect that much of this looks foreign.
    Nicely indented, commented, vaguely familiar, but still somewhat foreign.
-   Many people these days generate JavaScript.  Popular with React is something
+   Many people these days generate JavaScript.  Popular with Vue is something
    called [JSX](http://facebook.github.io/react/docs/jsx-in-depth.html), but
    that's both controversial and [doesn't support if
    statements](http://facebook.github.io/react/tips/if-else-in-JSX.html).
@@ -298,12 +292,12 @@ Now onto the tests:
     (expressed in Ruby, but compiled to JavaScript) can be tested.  It does so
     by setting up a http server (the code for which is in
     [spec/react_server.rb](spec/react_server.rb)) which runs arbitrary scripts
-    and returns the results as HTML.  This approach excels at testing a React
+    and returns the results as HTML.  This approach excels at testing a Vue
     component.
 
   * [spec/client_spec.rb](spec/client_spec.rb) takes this a bit further to
     do a client side unit test.  Instance variables set in tests are passed
-    to the React server, and arbitrary JavaScript code can be executed using
+    to the Vue server, and arbitrary JavaScript code can be executed using
     this data.  Output is in the form of XHTML-style tags which is then
     matched against CSS (or xpath) expressions.
 
@@ -390,7 +384,7 @@ would involve:
     [views/layout/header.js.rb](views/layout/header.js.rb)
   * Adding the path to the `route` method in
     [views/router.js.rb](views/router.js.rb)
-  * Adding a React component for the page to `views/pages`
+  * Adding a Vue component for the page to `views/pages`
   * Adding any new files to [views/app.js.rb](views/app.js.rb)
   * Adding a specification to
     [specs/other_views_specs.rb](specs/other_views_specs.rb)
@@ -399,7 +393,7 @@ Adding a new modal dialog would involve:
 
   * Adding a entry to the buttons list in
     [views/models/agenda.rb](views/models/agenda.rb)
-  * Adding a React component for the form to `views/forms`
+  * Adding a Vue component for the form to `views/forms`
   * Adding a server side action to `views/actions`.  A number of [actions
     from the current agenda
     tool](https://svn.apache.org/repos/infra/infrastructure/trunk/projects/whimsy/www/board/agenda/json)
@@ -423,7 +417,7 @@ Nothing is perfect.  Here are a few things to watch out for:
    [Ruby2JS filters](https://github.com/rubys/ruby2js#filters) reduce this
    gap by converting many common Ruby methods calls to JavaScript equivalents
    (e.g., `a.include? b` becomes `a.indexOf(b) != -1`).  Currently the
-   agenda tool makes use of the `react`, `functions` and `require` filters.
+   agenda tool makes use of the `vue`, `functions` and `require` filters.
 
  * In Ruby there isn't a difference between accessing attributes and methods
    which have no arguments.  In JavaScript there is.  To make this work,
@@ -442,7 +436,7 @@ Nothing is perfect.  Here are a few things to watch out for:
 
  * In Ruby, `$` is not a legal method name, so this common
    alias for `jQuery` isn't directly available.  jQuery isn't needed for
-   react, but is needed for Bootstrap.  As such there will be few places where
+   vue, but is needed for Bootstrap.  As such there will be few places where
    this will be needed.  As previously mentioned, I've considered using
    the `~` operator for this.
 
@@ -455,7 +449,7 @@ Further reading:
    framework for developing responsive, mobile first projects on the web
  * [capybara](https://github.com/jnicklas/capybara#readme) - helps you test
    web applications by simulating how a real user would interact with your app
- * [react](http://facebook.github.io/react/) - a JavaScript library for
+ * [vue](https://vuejs.org/) - a JavaScript library for
    building user interfaces 
  * [ruby2js](https://github.com/rubys/ruby2jw/#readme) - minimal yet
    extensible Ruby to JavaScript conversion. 
diff --git a/www/board/agenda/main.rb b/www/board/agenda/main.rb
index 3c41022..d294a29 100755
--- a/www/board/agenda/main.rb
+++ b/www/board/agenda/main.rb
@@ -8,7 +8,7 @@ require 'whimsy/asf/agenda'
 require 'whimsy/asf/board'
 
 require 'wunderbar/sinatra'
-require 'wunderbar/react'
+require 'wunderbar/vue'
 require 'wunderbar/bootstrap/theme'
 require 'ruby2js/filter/functions'
 require 'ruby2js/filter/require'
diff --git a/www/board/agenda/package.json b/www/board/agenda/package.json
index 9d958e1..6ea8762 100644
--- a/www/board/agenda/package.json
+++ b/www/board/agenda/package.json
@@ -8,7 +8,6 @@
   "devDependencies": {
     "jsdom": "^11.1.0",
     "jquery": "^3.2.1",
-    "react": "^15.6.1",
-    "react-dom": "^15.6.1"
+    "vue": "^2.4.4"
   }
 }
diff --git a/www/board/agenda/views/app.js.rb b/www/board/agenda/views/app.js.rb
index 1366739..c7ba655 100644
--- a/www/board/agenda/views/app.js.rb
+++ b/www/board/agenda/views/app.js.rb
@@ -1,3 +1,6 @@
+# config
+require_relative 'vue-config'
+
 # common
 require_relative 'router'
 require_relative 'keyboard'
diff --git a/www/board/agenda/views/buttons/add-comment.js.rb b/www/board/agenda/views/buttons/add-comment.js.rb
index 2445424..5632f01 100644
--- a/www/board/agenda/views/buttons/add-comment.js.rb
+++ b/www/board/agenda/views/buttons/add-comment.js.rb
@@ -8,7 +8,7 @@
 # form is dismissed.
 #
 
-class AddComment < React
+class AddComment < Vue
   def initialize
     @base = @comment = @@item.pending
     @disabled = false
diff --git a/www/board/agenda/views/buttons/add-minutes.js.rb b/www/board/agenda/views/buttons/add-minutes.js.rb
index 8f088bd..65ca1a8 100644
--- a/www/board/agenda/views/buttons/add-minutes.js.rb
+++ b/www/board/agenda/views/buttons/add-minutes.js.rb
@@ -1,4 +1,4 @@
-class AddMinutes < React
+class AddMinutes < Vue
   def initialize
     @disabled = false
   end
diff --git a/www/board/agenda/views/buttons/approve.js.rb b/www/board/agenda/views/buttons/approve.js.rb
index fbd1445..d1c1ae6 100644
--- a/www/board/agenda/views/buttons/approve.js.rb
+++ b/www/board/agenda/views/buttons/approve.js.rb
@@ -1,7 +1,7 @@
 #
 # Approve/Unapprove a report
 #
-class Approve < React
+class Approve < Vue
   def initialize
     @disabled = false
     @request = 'approve'
diff --git a/www/board/agenda/views/buttons/attend.js.rb b/www/board/agenda/views/buttons/attend.js.rb
index 40e5621..7d0dd9e 100644
--- a/www/board/agenda/views/buttons/attend.js.rb
+++ b/www/board/agenda/views/buttons/attend.js.rb
@@ -1,7 +1,7 @@
 #
 # Indicate intention to attend / regrets for meeting
 #
-class Attend < React
+class Attend < Vue
   def initialize
     @disabled = false
   end
diff --git a/www/board/agenda/views/buttons/commit.js.rb b/www/board/agenda/views/buttons/commit.js.rb
index fd7264b..eacf2f1 100644
--- a/www/board/agenda/views/buttons/commit.js.rb
+++ b/www/board/agenda/views/buttons/commit.js.rb
@@ -3,7 +3,7 @@
 # and allow it to be changed.
 #
 
-class Commit < React
+class Commit < Vue
   def initialize
     @disabled = false
   end
diff --git a/www/board/agenda/views/buttons/draft-minutes.js.rb b/www/board/agenda/views/buttons/draft-minutes.js.rb
index bdf9765..2b506b1 100644
--- a/www/board/agenda/views/buttons/draft-minutes.js.rb
+++ b/www/board/agenda/views/buttons/draft-minutes.js.rb
@@ -1,4 +1,4 @@
-class DraftMinutes < React
+class DraftMinutes < Vue
   def initialize
     @disabled = true
   end
diff --git a/www/board/agenda/views/buttons/email.js.rb b/www/board/agenda/views/buttons/email.js.rb
index 169a068..fa70ffc 100644
--- a/www/board/agenda/views/buttons/email.js.rb
+++ b/www/board/agenda/views/buttons/email.js.rb
@@ -2,7 +2,7 @@
 # Send email
 #
 
-class Email < React
+class Email < Vue
   def render
     _button.btn 'send email', class: self.mailto_class(),
       onClick: self.launch_email_client
diff --git a/www/board/agenda/views/buttons/markseen.js.rb b/www/board/agenda/views/buttons/markseen.js.rb
index 4d029cc..96cc182 100644
--- a/www/board/agenda/views/buttons/markseen.js.rb
+++ b/www/board/agenda/views/buttons/markseen.js.rb
@@ -1,7 +1,7 @@
 #
 # A button that mark all comments as 'seen', with an undo option
 #
-class MarkSeen < React
+class MarkSeen < Vue
   def initialize
     @disabled = false
     @label = 'mark seen'
diff --git a/www/board/agenda/views/buttons/message.js.rb b/www/board/agenda/views/buttons/message.js.rb
index f437327..fc0d3bb 100644
--- a/www/board/agenda/views/buttons/message.js.rb
+++ b/www/board/agenda/views/buttons/message.js.rb
@@ -1,7 +1,7 @@
 #
 # Message area for backchannel
 #
-class Message < React
+class Message < Vue
   def initialize
     @disabled = false
     @message = ''
diff --git a/www/board/agenda/views/buttons/post-actions.js.rb b/www/board/agenda/views/buttons/post-actions.js.rb
index f96f3ef..375337f 100644
--- a/www/board/agenda/views/buttons/post-actions.js.rb
+++ b/www/board/agenda/views/buttons/post-actions.js.rb
@@ -1,7 +1,7 @@
 #
 # Indicate intention to attend / regrets for meeting
 #
-class PostActions < React
+class PostActions < Vue
   def initialize
     @disabled = false
   end
diff --git a/www/board/agenda/views/buttons/post.js.rb b/www/board/agenda/views/buttons/post.js.rb
index 757cbd8..22824f4 100644
--- a/www/board/agenda/views/buttons/post.js.rb
+++ b/www/board/agenda/views/buttons/post.js.rb
@@ -4,7 +4,7 @@
 # For new resolutions, allow entry of title, but not commit message
 # For everything else, allow modification of commit message, but not title
 
-class Post < React
+class Post < Vue
   def initialize
     @disabled = false
     @alerted = false
@@ -62,13 +62,8 @@ class Post < React
     end
   end
 
-  # set properties on initial load
-  def componentWillMount()
-    self.componentWillReceiveProps()
-  end
-
   # autofocus on report/resolution title/text
-  def componentDidMount()
+  def mounted()
     jQuery('#post-report-form').on 'shown.bs.modal' do
       if @@button.text == 'add resolution'
         ~'#post-report-title'.focus()
@@ -79,7 +74,7 @@ class Post < React
   end
 
   # match form title, input label, and commit message with button text
-  def componentWillReceiveProps(newprops)
+  def created(newprops)
     case @@button.text
     when 'post report'
       @header = 'Post Report'
diff --git a/www/board/agenda/views/buttons/publish-minutes.js.rb b/www/board/agenda/views/buttons/publish-minutes.js.rb
index fe80bef..c4f446b 100644
--- a/www/board/agenda/views/buttons/publish-minutes.js.rb
+++ b/www/board/agenda/views/buttons/publish-minutes.js.rb
@@ -1,4 +1,4 @@
-class PublishMinutes < React
+class PublishMinutes < Vue
   def initialize
     @disabled = false
     @previous_title = nil
diff --git a/www/board/agenda/views/buttons/refresh.js.rb b/www/board/agenda/views/buttons/refresh.js.rb
index 9962f5d..5a10062 100644
--- a/www/board/agenda/views/buttons/refresh.js.rb
+++ b/www/board/agenda/views/buttons/refresh.js.rb
@@ -1,7 +1,7 @@
 #
 # A button that will do a 'svn update' of the agenda on the server
 #
-class Refresh < React
+class Refresh < Vue
   def initialize
     @disabled = false
   end
diff --git a/www/board/agenda/views/buttons/reminders.js.rb b/www/board/agenda/views/buttons/reminders.js.rb
index 62e852e..f0506c2 100644
--- a/www/board/agenda/views/buttons/reminders.js.rb
+++ b/www/board/agenda/views/buttons/reminders.js.rb
@@ -3,7 +3,7 @@
 # associated button) as well as a second button.
 #
 
-class InitialReminder < React
+class InitialReminder < Vue
   def initialize
     @disabled = true
     @subject = ''
@@ -108,7 +108,7 @@ end
 #
 # A button for final reminders
 #
-class FinalReminder < React
+class FinalReminder < Vue
   def render
     _button.btn.btn_primary 'send final reminders', 
       data_toggle: 'modal', data_target: '#reminder-form'
diff --git a/www/board/agenda/views/buttons/showseen.js.rb b/www/board/agenda/views/buttons/showseen.js.rb
index c3c5771..3f31724 100644
--- a/www/board/agenda/views/buttons/showseen.js.rb
+++ b/www/board/agenda/views/buttons/showseen.js.rb
@@ -1,7 +1,7 @@
 #
 # Show/hide seen items
 #
-class ShowSeen < React
+class ShowSeen < Vue
   def initialize
     @label = 'show seen'
   end
diff --git a/www/board/agenda/views/buttons/timestamp.js.rb b/www/board/agenda/views/buttons/timestamp.js.rb
index e6e73db..ca3cb5c 100644
--- a/www/board/agenda/views/buttons/timestamp.js.rb
+++ b/www/board/agenda/views/buttons/timestamp.js.rb
@@ -1,7 +1,7 @@
 #
 # Timestamp start/stop of meeting
 #
-class Timestamp < React
+class Timestamp < Vue
   def initialize
     @disabled = false
   end
diff --git a/www/board/agenda/views/buttons/vote.js.rb b/www/board/agenda/views/buttons/vote.js.rb
index ed34a12..b988e33 100644
--- a/www/board/agenda/views/buttons/vote.js.rb
+++ b/www/board/agenda/views/buttons/vote.js.rb
@@ -1,4 +1,4 @@
-class Vote < React
+class Vote < Vue
   def initialize
     @disabled = false
   end
diff --git a/www/board/agenda/views/elements/additional-info.js.rb b/www/board/agenda/views/elements/additional-info.js.rb
index b6d9774..c7b524f 100644
--- a/www/board/agenda/views/elements/additional-info.js.rb
+++ b/www/board/agenda/views/elements/additional-info.js.rb
@@ -13,7 +13,7 @@
 #       are unique.
 #
 
-class AdditionalInfo < React
+class AdditionalInfo < Vue
   def render
     # special notes
     _p.notes @@item.notes if @@item.notes
diff --git a/www/board/agenda/views/elements/info.js.rb b/www/board/agenda/views/elements/info.js.rb
index be3ffe7..f2862eb 100644
--- a/www/board/agenda/views/elements/info.js.rb
+++ b/www/board/agenda/views/elements/info.js.rb
@@ -1,4 +1,4 @@
-class Info < React
+class Info < Vue
   def render
     _dl.dl_horizontal class: @@position do
       _dt 'Attach'
diff --git a/www/board/agenda/views/elements/link.js.rb b/www/board/agenda/views/elements/link.js.rb
index 96a8db9..66a3d93 100644
--- a/www/board/agenda/views/elements/link.js.rb
+++ b/www/board/agenda/views/elements/link.js.rb
@@ -3,34 +3,31 @@
 # processed locally by calling Main.navigate.
 #
 
-class Link < React
-  def initialize
-    @attrs = {}
+class Link < Vue
+  def render
+    Vue.createElement(element, options, @@text)
   end
 
-  def componentWillMount()
-    self.componentWillReceiveProps()
-    @attrs.onClick = self.click
+  def element
+    if @@href
+      'a'
+    else
+      'span'
+    end
   end
 
-  def componentWillReceiveProps(props)
-    @text = props.text
+  def options
+    result = {attrs: {}}
 
-    for attr in props
-      next unless props[attr]
-      @attrs[attr] = props[attr] unless attr == 'text'
+    if @@href
+      result.attrs.href = @@href.gsub(%r{(^|/)\w+/\.\.(/|$)}, '$1')
     end
 
-    if props.href
-      @element = 'a'
-      @attrs.href = props.href.gsub(%r{(^|/)\w+/\.\.(/|$)}, '$1')
-    else
-      @element = 'span'
-    end
-  end
+    result.attrs.rel = @@rel if @@rel
 
-  def render
-    React.createElement(@element, @attrs, @text)
+    result.on = {click: self.click}
+
+    result 
   end
 
   def click(event)
diff --git a/www/board/agenda/views/elements/modal-dialog.js.rb b/www/board/agenda/views/elements/modal-dialog.js.rb
index e4970c7..c668eab 100644
--- a/www/board/agenda/views/elements/modal-dialog.js.rb
+++ b/www/board/agenda/views/elements/modal-dialog.js.rb
@@ -5,66 +5,61 @@
 # distributed to header, body, and footer sections.
 #
 
-class ModalDialog < React
+class ModalDialog < Vue
   def initialize
     @header = []
     @body = []
     @footer = []
   end
 
-  def componentWillMount()
-    self.componentWillReceiveProps()
-  end
-
-  def componentWillReceiveProps()
+  def created()
     @header.clear()
     @body.clear()
     @footer.clear()
 
-    @@children.each do |child|
-      if child.type == 'h4'
+    $slots.default.each do |slot|
+      if slot.tag == 'h4'
 
         # place h4 elements into the header, adding a modal-title class
-        child = self.addClass(child, 'modal-title')
-        @header << child
-        ModalDialog.h4 = child
+        slot = self.addClass(slot, 'modal-title')
+        @header << slot
 
-      elsif child.type == 'button'
+      elsif slot.tag == 'button'
 
         # place button elements into the footer, adding a btn class
-        child = self.addClass(child, 'btn')
-        @footer << child
+        slot = self.addClass(slot, 'btn')
+        @footer << slot
 
-      elsif child.type == 'input' or child.type == 'textarea'
+      elsif slot.tag == 'input' or slot.tag == 'textarea'
 
         # wrap input and textarea elements in a form-control, 
         # add label if present
 
-        child = self.addClass(child, 'form-control')
+        slot = self.addClass(slot, 'form-control')
 
         label = nil
-        if child.props.label and child.props.id
-          props = {htmlFor: child.props.id}
-          if child.props.type == 'checkbox'
-            props.className = 'checkbox'
-            label = React.createElement('label', props, child,
-              child.props.label)
-            child.props.delete 'label'
-            child = nil
+        if slot.data.attrs.label and slot.data.attrs.id
+          props = {attrs: {for: slot.data.attrs.id}}
+          if slot.data.attrs.type == 'checkbox'
+            props.class = ['checkbox']
+            label = Vue.createElement('label', props, [slot,
+              slot.data.attrs.label])
+            slot.data.attrs.delete 'label'
+            slot = nil
           else
-            label = React.createElement('label', props, child.props.label)
-            child = React.cloneElement(child, label: nil)
+            label = Vue.createElement('label', props, slot.data.attrs.label)
+            slot.data.attrs.delete 'label'
           end
         end
 
-        @body << React.createElement('div', {className: 'form-group'}, 
-          label, child)
+        @body << Vue.createElement('div', {class: 'form-group'}, 
+          [label, slot])
 
       else
 
         # place all other elements into the body
 
-        @body << child
+        @body << slot
       end
     end
   end
@@ -92,11 +87,11 @@ class ModalDialog < React
 
   # helper method: add a class to an element, returning new element
   def addClass(element, name)
-    if not element.props.className
-      element = React.cloneElement(element, className: name)
-    elsif not element.props.className.split(' ').include? name
-      element = React.cloneElement(element, 
-        className: element.props.className + " #{name}")
+    element.data ||= {}
+    if not element.data.class
+      element.data.class = [name]
+    elsif not element.data.class.include? name
+      element.data.class << name
     end
 
     return element
diff --git a/www/board/agenda/views/elements/pns.rb b/www/board/agenda/views/elements/pns.rb
index 8ef23a3..7d910ee 100644
--- a/www/board/agenda/views/elements/pns.rb
+++ b/www/board/agenda/views/elements/pns.rb
@@ -2,7 +2,7 @@
 # Determine status of podling name
 #
 
-class PodlingNameSearch < React
+class PodlingNameSearch < Vue
   def render
     _span.pns title: 'podling name search' do
       if Server.podlingnamesearch
diff --git a/www/board/agenda/views/elements/text.js.rb b/www/board/agenda/views/elements/text.js.rb
index 6b6ca02..f034688 100644
--- a/www/board/agenda/views/elements/text.js.rb
+++ b/www/board/agenda/views/elements/text.js.rb
@@ -2,20 +2,18 @@
 # Escape text for inclusion in HTML; optionally apply filters
 #
 
-class Text < React
-  def componentWillMount()
-    self.componentWillReceiveProps()
+class Text < Vue
+  def render
+    _span domPropsInnerHTML: text
   end
 
-  def componentWillReceiveProps()
-    @text = htmlEscape(@@raw || '')
+  def text
+    result = htmlEscape(@@raw || '')
 
     if @@filters
-      @@filters.each { |filter| @text = filter(@text) }
+      @@filters.each { |filter| result = filter(result) }
     end
-  end
 
-  def render
-    _span dangerouslySetInnerHTML: { __html: @text }
+    result
   end
 end
diff --git a/www/board/agenda/views/keyboard.js.rb b/www/board/agenda/views/keyboard.js.rb
index 4edfc1c..a3ffb80 100644
--- a/www/board/agenda/views/keyboard.js.rb
+++ b/www/board/agenda/views/keyboard.js.rb
@@ -7,19 +7,21 @@ class Keyboard
 
     # keyboard navigation (unless on the search screen)
     def (document.body).onkeydown(event)
-      return if ~'#search-text'[0] or ~'.modal-open'[0] or ~'.modal.in'[0]
+      return if document.getElementById('search-text') or
+        document.querySelector('modal-open') or
+        document.querySelector('modal-in')
       return if not event.altKey and
         %w(input textarea).include? document.activeElement.tagName.downcase()
       return if event.metaKey or event.ctrlKey
 
       if event.keyCode == 37 # '<-'
-        link = ~"a[rel=prev]"[0]
+        link = document.querySelector("a[rel=prev]")
         if link
           link.click()
           return false
         end
       elsif event.keyCode == 39 # '->'
-        link = ~"a[rel=next]"[0]
+        link = document.querySelector("a[rel=next]")
         if link
           link.click()
           return false
diff --git a/www/board/agenda/views/layout/footer.js.rb b/www/board/agenda/views/layout/footer.js.rb
index 3a8d73a..aff616c 100644
--- a/www/board/agenda/views/layout/footer.js.rb
+++ b/www/board/agenda/views/layout/footer.js.rb
@@ -8,7 +8,7 @@
 #  last flagged <-> first Special order)
 #
 
-class Footer < React
+class Footer < Vue
   def render
     _footer.navbar.navbar_fixed_bottom class: @@item.color do
 
@@ -71,9 +71,9 @@ class Footer < React
         if @@buttons
           @@buttons.each do |button|
             if button.text
-              React.createElement('button', button.attrs, button.text)
+              Vue.createElement('button', {attrs: button.attrs}, button.text)
             elsif button.type
-              React.createElement(button.type, button.attrs)
+              Vue.createElement(button.type, {props: button.attrs})
             end
           end
         end
diff --git a/www/board/agenda/views/layout/header.js.rb b/www/board/agenda/views/layout/header.js.rb
index 016906d..7d80926 100644
--- a/www/board/agenda/views/layout/header.js.rb
+++ b/www/board/agenda/views/layout/header.js.rb
@@ -5,7 +5,7 @@
 #
 # Finally: make info dropdown status 'sticky'
 
-class Header < React
+class Header < Vue
   def initialize
     @infodropdown = nil
   end
diff --git a/www/board/agenda/views/layout/main.js.rb b/www/board/agenda/views/layout/main.js.rb
index 607895f..a2de545 100644
--- a/www/board/agenda/views/layout/main.js.rb
+++ b/www/board/agenda/views/layout/main.js.rb
@@ -8,7 +8,7 @@
 #  * Resizing view to leave room for the Header and Footer
 #
 
-class Main < React
+class Main < Vue
   # common layout for all pages: header, main, footer, and forms
   def render
     if not @item
@@ -19,8 +19,8 @@ class Main < React
 
       view = nil
       _main do
-        React.createElement(@item.view, item: @item,
-         ref: proc {|component| Main.view=component})
+        Vue.createElement(@item.view, props: {item: @item,
+         ref: proc {|component| Main.view=component}})
       end
 
       _Footer item: @item, buttons: @buttons, options: @options
@@ -29,8 +29,8 @@ class Main < React
       if @buttons
         @buttons.each do |button|
           if button.form
-            React.createElement(button.form, item: @item, server: Server,
-              button: button)
+            Vue.createElement(button.form, props: {item: @item, server: Server,
+              button: button})
           end
         end
       end
@@ -38,7 +38,7 @@ class Main < React
   end
 
   # initial load of the agenda, and route first request
-  def componentWillMount()
+  def created()
     # copy server info for later use
     for prop in @@server
       Server[prop] = @@server[prop]
@@ -85,7 +85,7 @@ class Main < React
   end
 
   # additional client side initialization
-  def componentDidMount()
+  def mounted()
     # export navigate and refresh methods
     Main.navigate = self.navigate
     Main.refresh  = self.refresh
@@ -118,7 +118,7 @@ class Main < React
     # whenever the window is resized, adjust margins of the main area to
     # avoid overlapping the header and footer areas
     def window.onresize()
-      main = ~'main'
+      main = document.querySelector('main')
       if 
         window.innerHeight <= 400 and 
         document.body.scrollHeight > window.innerHeight
@@ -130,8 +130,10 @@ class Main < React
       else
         document.querySelector('footer').style.position = 'fixed'
         document.querySelector('header').style.position = 'fixed'
-        main.style.marginTop = "#{~'header.navbar'.clientHeight}px"
-        main.style.marginBottom = "#{~'footer.navbar'.clientHeight}px"
+        main.style.marginTop = 
+          "#{document.querySelector('header.navbar').clientHeight}px"
+        main.style.marginBottom = 
+          "#{document.querySelector('footer.navbar').clientHeight}px"
       end
 
       if Main.scrollTo == 0 or Main.scrollTo
@@ -160,7 +162,7 @@ class Main < React
   end
 
   # after each subsequent re-rendering, resize main window
-  def componentDidUpdate()
+  def updated()
     window.onresize()
   end
 end
diff --git a/www/board/agenda/views/pages/action-items.js.rb b/www/board/agenda/views/pages/action-items.js.rb
index e83fb32..c2d033f 100644
--- a/www/board/agenda/views/pages/action-items.js.rb
+++ b/www/board/agenda/views/pages/action-items.js.rb
@@ -3,7 +3,7 @@
 # action item status updates.
 #
 
-class ActionItems < React
+class ActionItems < Vue
   def initialize
     @disabled = false
   end
@@ -73,19 +73,20 @@ class ActionItems < React
           end
 
           # launch edit dialog when there is a click on the status
-          attrs = {onClick: self.updateStatus, className: 'clickable'}
-          attrs = {} if Minutes.complete
+          options = {on: {click: self.updateStatus}, class: ['clickable']}
+          options = {} if Minutes.complete
+          options.attrs = {}
 
           # copy action properties to data attributes
           for name in action
-            attrs["data-#{name}"] = action[name]
+            options.attrs["data-#{name}"] = action[name]
           end
 
           # include pending updates
           pending = Pending.find_status(action)
           attrs['data-status'] = pending.status if pending
 
-          React.createElement('span', attrs) do
+          Vue.createElement('span', options) do
             # highlight missing action item status updates
             if pending
               _span "Status: "
diff --git a/www/board/agenda/views/pages/adjournment.js.rb b/www/board/agenda/views/pages/adjournment.js.rb
index dc41fd3..28556b0 100644
--- a/www/board/agenda/views/pages/adjournment.js.rb
+++ b/www/board/agenda/views/pages/adjournment.js.rb
@@ -2,7 +2,7 @@
 # Secretary version of Adjournment section: shows todos
 #
 
-class Adjournment < React
+class Adjournment < Vue
   def initialize
     Todos.set({
       add: [],
@@ -123,7 +123,7 @@ end
 #                          Add, Remove chairs                          #
 ########################################################################
 
-class TodoActions < React
+class TodoActions < Vue
   def initialize
     @checked = {}
     @disabled = true
@@ -219,7 +219,7 @@ end
 #                          Establish actions                           #
 ########################################################################
 
-class EstablishActions < React
+class EstablishActions < Vue
   def initialize
     @checked = {}
     @disabled = true
@@ -308,7 +308,7 @@ end
 #                      Reminder to draft feedback                      #
 ########################################################################
 
-class FeedbackReminder < React
+class FeedbackReminder < Vue
   def render
     _p 'Draft feedback:'
 
diff --git a/www/board/agenda/views/pages/backchannel.js.rb b/www/board/agenda/views/pages/backchannel.js.rb
index 47763b1..c45af45 100644
--- a/www/board/agenda/views/pages/backchannel.js.rb
+++ b/www/board/agenda/views/pages/backchannel.js.rb
@@ -2,7 +2,7 @@
 # Overall Agenda page: simple table with one row for each item in the index
 #
 
-class Backchannel < React
+class Backchannel < Vue
   # place a message input field in the buttons area
   def self.buttons()
     return [{button: Message}]
diff --git a/www/board/agenda/views/pages/bootstrap.js.rb b/www/board/agenda/views/pages/bootstrap.js.rb
index 202c0e9..7a1a578 100644
--- a/www/board/agenda/views/pages/bootstrap.js.rb
+++ b/www/board/agenda/views/pages/bootstrap.js.rb
@@ -2,7 +2,7 @@
 # Blank canvas shown during bootstrapping
 #
 
-class BootStrapPage < React
+class BootStrapPage < Vue
   def render
     _p ''
   end
diff --git a/www/board/agenda/views/pages/cache.js.rb b/www/board/agenda/views/pages/cache.js.rb
index 8f04b7b..d3e6fff 100644
--- a/www/board/agenda/views/pages/cache.js.rb
+++ b/www/board/agenda/views/pages/cache.js.rb
@@ -2,7 +2,7 @@
 # A page showing status of caches and service workers
 #
 
-class CacheStatus < React
+class CacheStatus < Vue
   def self.buttons()
     return [{button: ClearCache}, {button: UnregisterWorker}]
   end
@@ -92,7 +92,7 @@ end
 #
 # A button that clear the cache
 #
-class ClearCache < React
+class ClearCache < Vue
   def initialize
     @disabled = true
   end
@@ -131,7 +131,7 @@ end
 # A button that removes the service worker.  Sadly, it doesn't seem to have
 # any affect on the list of registrations that is dynamically returned.
 #
-class UnregisterWorker < React
+class UnregisterWorker < Vue
   def render
     _button.btn.btn_primary 'Unregister ServiceWorker', onClick: self.click
   end 
@@ -156,7 +156,7 @@ end
 # Individual Cache page
 #
 
-class CachePage < React
+class CachePage < Vue
   def initialize
     @response = {}
     @text = ''
diff --git a/www/board/agenda/views/pages/comments.js.rb b/www/board/agenda/views/pages/comments.js.rb
index a7d8b26..a7a2411 100644
--- a/www/board/agenda/views/pages/comments.js.rb
+++ b/www/board/agenda/views/pages/comments.js.rb
@@ -3,7 +3,7 @@
 # Conditionally hide comments previously marked as seen.
 #
 
-class Comments < React
+class Comments < Vue
   def self.buttons()
     buttons = []
 
diff --git a/www/board/agenda/views/pages/flagged.js.rb b/www/board/agenda/views/pages/flagged.js.rb
index 9afbb99..a5cf73d 100644
--- a/www/board/agenda/views/pages/flagged.js.rb
+++ b/www/board/agenda/views/pages/flagged.js.rb
@@ -2,7 +2,7 @@
 # A page showing all flagged reports
 #
 
-class Flagged < React
+class Flagged < Vue
   def render
     first = true
 
diff --git a/www/board/agenda/views/pages/fy22.js.rb b/www/board/agenda/views/pages/fy22.js.rb
index b5e7023..1ff5405 100644
--- a/www/board/agenda/views/pages/fy22.js.rb
+++ b/www/board/agenda/views/pages/fy22.js.rb
@@ -1,7 +1,7 @@
 #
 # FY22 budget worksheet
 #
-class FY22 < React
+class FY22 < Vue
   def initialize
     @budget = (Minutes.started && Minutes.get('budget')) || {
       donations: 110,
diff --git a/www/board/agenda/views/pages/help.js.rb b/www/board/agenda/views/pages/help.js.rb
index dddfef9..091dddd 100644
--- a/www/board/agenda/views/pages/help.js.rb
+++ b/www/board/agenda/views/pages/help.js.rb
@@ -1,4 +1,4 @@
-class Help < React
+class Help < Vue
   def render
     _h3 'Keyboard shortcuts'
     _dl.dl_horizontal do
diff --git a/www/board/agenda/views/pages/index.js.rb b/www/board/agenda/views/pages/index.js.rb
index 58424a8..7fbed65 100644
--- a/www/board/agenda/views/pages/index.js.rb
+++ b/www/board/agenda/views/pages/index.js.rb
@@ -2,7 +2,7 @@
 # Overall Agenda page: simple table with one row for each item in the index
 #
 
-class Index < React
+class Index < Vue
   def render
     _header do
       _h1 'ASF Board Agenda'
diff --git a/www/board/agenda/views/pages/missing.js.rb b/www/board/agenda/views/pages/missing.js.rb
index db407bb..e48d089 100644
--- a/www/board/agenda/views/pages/missing.js.rb
+++ b/www/board/agenda/views/pages/missing.js.rb
@@ -2,7 +2,7 @@
 # A page showing all flagged reports
 #
 
-class Missing < React
+class Missing < Vue
   def initialize
     @checked = {}
   end
diff --git a/www/board/agenda/views/pages/queue.js.rb b/www/board/agenda/views/pages/queue.js.rb
index df10e3f..2c3d43e 100644
--- a/www/board/agenda/views/pages/queue.js.rb
+++ b/www/board/agenda/views/pages/queue.js.rb
@@ -3,7 +3,7 @@
 # that are ready for review.
 #
 
-class Queue < React
+class Queue < Vue
   def self.buttons()
     buttons = [{button: Refresh}]
     buttons << {form: Commit} if Pending.count > 0
diff --git a/www/board/agenda/views/pages/report.js.rb b/www/board/agenda/views/pages/report.js.rb
index 89e5d48..4959a93 100644
--- a/www/board/agenda/views/pages/report.js.rb
+++ b/www/board/agenda/views/pages/report.js.rb
@@ -12,7 +12,7 @@
 # Filters may be used to highlight or hypertext link portions of the text.
 #
 
-class Report < React
+class Report < Vue
   def render
     _section.flexbox do
       _section do
@@ -56,12 +56,7 @@ class Report < React
     end
   end
 
-  # check for additional actions on initial render
-  def componentWillMount()
-    self.componentWillReceiveProps()
-  end
-
-  def componentWillReceiveProps()
+  def created()
     # determine what text filters to run
     @filters = [self.linebreak, self.todo, hotlink, self.privates, self.jira]
     @filters = [self.localtime, hotlink] if @@item.title == 'Call to order'
diff --git a/www/board/agenda/views/pages/roll-call.js.rb b/www/board/agenda/views/pages/roll-call.js.rb
index 85458b8..08484d9 100644
--- a/www/board/agenda/views/pages/roll-call.js.rb
+++ b/www/board/agenda/views/pages/roll-call.js.rb
@@ -1,7 +1,7 @@
 #
 # Secretary Roll Call update form
 
-class RollCall < React
+class RollCall < Vue
   def initialize
     RollCall.lockFocus = false
     @guest = ''
@@ -136,7 +136,7 @@ end
 #
 # An individual attendee (Director, Executive Officer, or Guest)
 #
-class Attendee < React
+class Attendee < Vue
   def initialize
     # last posted value for notes for this attendee
     @base = ''
diff --git a/www/board/agenda/views/pages/search.js.rb b/www/board/agenda/views/pages/search.js.rb
index 2c1a9bf..1ca3543 100644
--- a/www/board/agenda/views/pages/search.js.rb
+++ b/www/board/agenda/views/pages/search.js.rb
@@ -5,7 +5,7 @@
 #  * keep query string in window location URL in synch
 #
 
-class Search < React
+class Search < Vue
   # initialize query text based on data passed to the component
   def initialize
     @text = @@item.query || ''
diff --git a/www/board/agenda/views/pages/select-actions.rb b/www/board/agenda/views/pages/select-actions.rb
index 85f8223..6a93f4d 100644
--- a/www/board/agenda/views/pages/select-actions.rb
+++ b/www/board/agenda/views/pages/select-actions.rb
@@ -3,7 +3,7 @@
 # action item status updates.
 #
 
-class SelectActions < React
+class SelectActions < Vue
   def self.buttons()
     return [{button: PostActions}]
   end
@@ -37,7 +37,7 @@ class SelectActions < React
   end
 end
 
-class CandidateAction < React
+class CandidateAction < Vue
   def render
     _input type: 'checkbox', checked: !@@action.complete,
       onChange:-> {@@action.complete = !@@action.complete; self.forceUpdate()}
diff --git a/www/board/agenda/views/pages/shepherd.js.rb b/www/board/agenda/views/pages/shepherd.js.rb
index 54ae353..9e33b76 100644
--- a/www/board/agenda/views/pages/shepherd.js.rb
+++ b/www/board/agenda/views/pages/shepherd.js.rb
@@ -3,7 +3,7 @@
 # that are ready for review.
 #
 
-class Shepherd < React
+class Shepherd < Vue
   def initialize
     @disabled = false
     @followup = []

-- 
To stop receiving notification emails like this one, please contact
['"commits@whimsical.apache.org" <co...@whimsical.apache.org>'].