Best Practices - Coalescing app configs and addon configs

I’m building an addon that’s mostly UI blocks. From the dummy app, I’ve validated “everything works.”

Within the dummy app, I want to show different layout options (css styles for minifiedNav or fullWidthContainer vs fixedWidthContainer, etc.) so I’ve created a “Settings” service to handle state. It’s working just as I want it to at the moment.

The next thing I can’t figure out is how to connect application environment configs to my service to prepopulate the app owner’s intention.

All the documentation I can find is from the app’s perspective (Configuring Your App - Configuration - Ember Guides for example) and includes the app’s name which doesn’t work here (my addon name doesn’t work either).

I have a strange solution that works:

export default class SettingsService extends Service {
  @tracked appLayout = {
    @tracked fixedHeader: false,
    @tracked fixedNavigation: false,
    @tracked minifyNavigation: false,
    @tracked hideNavigation: false,
    @tracked topNavigation: false,
    @tracked darkMode: false,
    @tracked boxedLayout: false,
  };
  constructor(app){
    super(app);
    deepmerge(this, app.application.Firewater);
  }
}

But it feels like I have a Rube Goldberg mess of an operation I’d expect many Addon Developers want to do… I just can’t find the documentation! Any pointers?

It’s slightly wordy, but an addon can always access the app’s config/environment.js like this:

import { getOwner } from '@ember/application';

export default class SettingsService extends Service {
  grabConfig() {
    let env = getOwner(this).resolveRegistration('config:environment');
  }
}

There’s also an addon that provides a shorthand for that.

Thanks @ef4, that’s much simpler. I’ll give that a go and see if it works.

Slightly related follow up questions:

  1. Is there a simpler way to “track everything in this object” other than adding @tracked to every attribute?
  2. deepmerge as a lib is proving problematic. It doesn’t play well with ember install and I’m getting inconsistent update results. Is there a better way to deep merge an object? Object.assign seems like it’s a shallow copy.

For (1) you can use TrackedObject from tracked-built-ins, though I would suggest using it with care: most things in an app are better represented with a TrackedMap (when you have actual map/dictionary semantics) or a class with tracked properties (where the shape is well known ahead of time).

You can use any library for merging, install it directly via npm or yarn and as long as you also have ember-auto-import you can import it directly into your code.

I have used merge and mergeWith from lodash.

Got it. Thanks for the feedback. Really appreciate it.

Final result (working and for posterity):

./addon/services/settings.js

import Service from '@ember/service';
import { getOwner } from '@ember/application';
import {action} from '@ember/object';
import {TrackedObject} from "tracked-built-ins";
import { merge } from 'lodash';

export default class SettingsService extends Service {
  /* changes are tracked in the app */
  appLayout = new TrackedObject({
    fixedNavigation: false,
    minifyNavigation: false,
    boxedLayout: false,
  });
 /* static data that won't change */
  appMeta = {
    logo: "img/logo.png",
    title: "Example Application"
  };

  @action toggleSetting(path) {
    const parts = path.split('.');
    let currentObject = this[parts[0]];
    parts.slice(1, -1).forEach(part => { currentObject = currentObject[part] });
    currentObject[parts.slice(-1)] = !currentObject[parts.slice(-1)];
  }

  constructor(app) {
    super(app);
    let config = getOwner(this).resolveRegistration("config:environment").MyAddon || {};
    merge(this, config);
  }
}

This lets anyone including my adding use the MyAddon config namespace to override these values WHIILE ALSO allowing for user level settings toggled via a button:

<button {{on "click" (fn this.settings.toggleSetting "appLayout.fixedNavigation")}}>Fixed Navigation</button>