0

I have this component which I attach to an input to stylize it the way I want.

@Component({
  selector: 'input[ui-input]',
  template: '',
  styleUrls: ['./input.component.scss'],
})
export class InputComponent {}

I then use it in the html like below. I also have a button which I click that opens a folder selection also seen below.

<ui-form-field *ngIf="home$ | async">
  <label ui-label>Default Project Location</label>
  <input ui-input [(ngModel)]="home" />
  <div ui-form-field-actions>
    <button ui-button-fab (click)="selectFolder()">
      <fa-icon [icon]="browseFolderIcon"></fa-icon>
    </button>
  </div>
</ui-form-field>

The component that I have attached to this looks like this:

@Component({
  selector: 'app-settings',
  templateUrl: './settings.component.html',
  styleUrls: ['./settings.component.scss'],
})
export class SettingsComponent {
  home = '';
  home$ = of(this.settings.get<string>('default-save-location')).pipe(
    tap(i => (this.home = i))
  )

  selectFolder() {
    this.electron
      .openFolder()
      .pipe(
        tap(i => console.log(i)),
        tap(i => (this.home = i))
      )
      .subscribe();
  }
}

When the component loads the home gets set and renders the value into the input. However, when selectFolder() runs, the new value gets logged to the console, but the template doesn't update unless I click inside of the input then click outside of the input.

I don't have changeDetection set on any of my components either so they are all using the default change detection strategy.

Not sure if it is relevant, but I have this in the electron service:

@Injectable({ providedIn: 'root' })
export class ElectronService {
  private home = new BehaviorSubject('/');
  home$ = this.home.pipe(switchMap(() => this.#getItem<string>('path', 'home')));

  openFolder() {
    return this.#getItem<{ filePaths: string[] }>('open-folder').pipe(
      map(i => i.filePaths?.[0] ?? ''),
      take(1)
    );
  }

  #getItem<T, U = unknown>(key: string, ...args: U[]) {
    return new Observable<T>(sub => {
      window.ipcRenderer.once(key, (e, v) => {
        sub.next(v);
        sub.complete();
      });
      window.ipcRenderer.send(key, ...args);
    });
  }
}
Get Off My Lawn
  • 34,175
  • 38
  • 176
  • 338
  • Looks like I have to call `tap(() => this.cd.detectChanges())`, why do I need that why isn't it changing automatically? – Get Off My Lawn Nov 16 '22 at 02:49
  • I replaced your service by normal observables and it worked fine. Could you create a stackblitz link for the issue? – Jimmy Nov 16 '22 at 10:20
  • @jimmy So, I made a stackblitz, and it does work there, so maybe it has to do with electron opening a [dialog](https://www.electronjs.org/docs/latest/api/dialog#dialogshowopendialogbrowserwindow-options) interfering with the lifecycle hooks. – Get Off My Lawn Nov 16 '22 at 16:58
  • could it be because I am attaching to `window.ipcRenderer` which is outside of the zone? – Get Off My Lawn Nov 16 '22 at 17:03
  • yup, you're right, this is a known issue: https://stackoverflow.com/questions/52481383/angular-electron-view-does-not-update-after-model-changes – Jimmy Nov 16 '22 at 19:11

1 Answers1

0

I was able to solve this issue by creating a preloader in the electron main service with contextIsolation. I also added a dialog handler to handle opening of dialogs:

let win: BrowserWindow;

app.on('ready', () => {
  win = new BrowserWindow({
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: true,
      preload: path.join(__dirname, 'preload.js'),
    },
  });

  ipcMain.handle('dialog', (evt, method: any, ...params: any[]) => dialog[method](...params));
});

Next in the preloader, I added a context bridge to expose some functions to the renderer:

contextBridge.exposeInMainWorld('electron', {
  dialogOpen: (method: string, options: any) => ipcRenderer.invoke('dialog', method, options),
});

Next, within my renderer process I just need to call thedialogOpen function:

  openFolder() {
    return from(
      window.electron.dialogOpen('showOpenDialog', { properties: ['openDirectory'] })
    ).pipe(
      map(i => i.filePaths?.[0] ?? ''),
      take(1)
    );
  }

Not only does this fix the issue, but I no longer have to call window.ipcRenderer.send() and then create a listener to listen for a response. The function just returns a promise with the result!

Get Off My Lawn
  • 34,175
  • 38
  • 176
  • 338