How to create a plugin for an application written in svelte.js?

If I were writing an application in pure JS, I would make a plugin connection like this:

App.js

var App = function(){ /* ... */ };
//...
App.prototype.regPlugin= function ( atr1, atr2, ... ) { /* ... */ };
//...
App.prototype.sendToEventBus = function ( atr1, ... ) { /* ... */ };
//...
var app = new App();
//...
var appModules = {};
//...
document.onreadystatechange = function () {
   if ( document.readyState === 'complete' ){
      for ( var module in AppModules ) {
        if ( AppModules[ module ] ) {
        try {
            AppModules[ module ].init( app );
        } catch(er) {
            //...
        }
      }
   }
}
//...

plugin.js

var MyPlugin = function (){ /*...*/ };
//...
MyPlugin.prototype.init = function ( app ) {
    this.app = app;
    //...
    app.regPlugin( plugAtr0 );
    //...
};
//...
MyPlugin.prototype.handleAny = function(){
   this.app.sendToEventBus( /* my event */ );
};
//...
appModules.myPlugin = new MyPlugin();

How to similarly make a plugin to an application on svelte.js?
Custom Element is not very suitable for this.

Upvotes: 2

Views: 1372

Answers (2)

Bob Fanger
Bob Fanger

Reputation: 29897

That type of plugin setup would still work, look into the Client-side component API

With component.$set you can change props from your plugin to the svelte component.

When you adding listeners to you plugin/app from inside svelte you might need additional assignments data = myPlugin.data for svelte to be able to react to changes.

Upvotes: 1

rixo
rixo

Reputation: 25001

Well, you can do something very similar if you're so inclined. Svelte only provides you with an UI component you can render anywhere you want on your page. It doesn't take over your whole JS.

One thing is that your Svelte application will most probably be bundled (Rollup or Webpack) using ES import statement. That means your code will live in ES modules, and local variables are not automatically attached to the window object in ES modules. So you've got to make that explicit. So your code would become something like this:

App.js (presumably your application entry point)

import App from './App.svelte'

const app = new App({
  target: document.body,
  props: {
    name: 'world',
  },
})

const appModules = {}

// expose appModules as a global variable
window.appModules = appModules

document.onreadystatechange = function() {
  if (document.readyState === 'complete') {
    debugger
    for (var module in appModules) {
      if (appModules[module]) {
        try {
          appModules[module].init(app)
        } catch (er) {
          //...
        }
      }
    }
  }
}

So now, app is your root Svelte component. It would lives in an App.svelte file. Svelte lets you add instance methods to components by exporting const or function.

App.svelte

You can export const or function to have instance methods on a Svelte component.

<script>

  export function regPlugin(...) { ... }

  // or
  export const sentToEventBus(...) { ... }

</script>
...

And... Voilà? Is there anything more in your code?

One issue, maybe, with the above code is that the App component will be rendered before your plugins had a chance to register.

You can fix this with a prop in your App component. In order to be able to change the value of this prop from your "controller code", you can use the $set method of the component. You can also set the accessors option on your component. You can do this globally with a bundler plugin option, or you can enable it on individual components with <svelte:options>.

If you need to have some custom logic that run only once app is ready, you can do so in a "reactive statement".

App.svelte

<svelte:options accessors={true} />

<script>
  export function regPlugin() {}
  export function sentToEventBus() {}

  export let ready = false

  $: if (ready) {
    // code to run when ready
  }
</script>

{#if ready}
  <!-- content to show when ready (all plugins initialized) -->
  <!-- most likely, you'd put other Svelte components in there -->
{:else}
  <div>Loading...</div>
{/if}

You can then toggle this prop when the app is ready to start:

App.js

document.onreadystatechange = function() {
  if (document.readyState === 'complete') {
    for (var module in appModules) {
      ...
    }

    app.$set({ ready: true })
    // or
    app.ready = true
  }
}

Alternatively, you may prefer to move the plugin init code in your App component. Since you have a "static" piece of state here, in the appModules variable, you'd have to put it into the static <script context="module"> part of your component:

App.svelte

<script context="module">
  // this block only runs once, when the module is loaded (same as 
  // if it was code in the root of a .js file)

  // this variable will be visible in all App instances
  const appModules = {}

  // make the appModules variable visible to the plugins
  window.appModules = appModules

  // you can also have static function here
  export function registerPlugin(name, plugin) {
    appModules[name] = plugin
  }
</script>

<script>
  // in contrast, this block will be run for each new instance of App

  ...

  let ready

  document.onreadystatechange = function() {
    if (document.readyState === 'complete') {
      // NOTE appModules bellow is the same as the one above
      for (var module in appModules) {
        // ...
      }
      ready = true
    }
  }
</script>

{#if ready}
  ...
{/if}

The static function addPlugin would be accessible as a named export from other modules:

import { addPlugin } from './App.svelte'

This would probably be more suited in the context of a bundled app / app with modules, than attaching things to window (hence running into risks of conflict in the global namespace). Depends on what you're doing...

Upvotes: 2

Related Questions