Tutorials

How to Use Setters to Change Data

Learn how to create and use setters in Joystick to modify data in your database and handle user interactions.

This tutorial was written by AI

The examples and explanations have been reviewed for accuracy. If you find an error, please let us know.

In this tutorial, you'll learn how to create and use setters in Joystick to modify data in your database. Setters are the "write" operations in your API that handle creating, updating, and deleting data.

What You'll Build

By the end of this tutorial, you'll have:

  • A setter for creating new books
  • A setter for updating existing books
  • A setter for deleting books
  • Components that use these setters
  • Input validation and authorization
  • Error handling and success feedback

Prerequisites

Before starting this tutorial, make sure you have:

Step 1: Create Your First Setter

Let's start by creating a setter to add new books to our database. Create a new file for your book setters:

api/books/setters.js

import joystick from '@joystick.js/node';

const setters = {
  create_book: {
    input: {
      title: {
        type: 'string',
        required: true,
      },
      author: {
        type: 'string',
        required: true,
      },
      category: {
        type: 'string',
        required: true,
      },
      description: {
        type: 'string',
        required: false,
      },
    },
    set: (input = {}, context = {}) => {
      return process.databases.mongodb.collection('books').insertOne({
        _id: joystick.id(),
        title: input?.title,
        author: input?.author,
        category: input?.category,
        description: input?.description || '',
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      });
    },
  },
};

export default setters;

This setter:

  • Validates input using the input schema
  • Uses joystick.id() to generate a unique ID
  • Inserts the new book into the MongoDB collection
  • Adds timestamps for tracking when the book was created

Step 2: Register Your Setters

Just like getters, setters need to be registered in your API schema. Update your api/index.js file:

api/index.js

import book_getters from './books/getters.js';
import book_setters from './books/setters.js';

const api = {
  getters: {
    ...book_getters,
  },
  setters: {
    ...book_setters,
  },
};

export default api;

Step 3: Create a Form Component to Use Your Setter

Now let's create a component that uses our setter to add new books:

ui/components/add_book_form/index.js

import joystick, { set } from '@joystick.js/ui';

const AddBookForm = joystick.component({
  state: {
    loading: false,
    success: false,
    error: null,
  },
  events: {
    'submit form': (event = {}, instance = {}) => {
      event.preventDefault();
      
      instance.set_state({ 
        loading: true, 
        error: null, 
        success: false 
      });

      const form_data = new FormData(event.target);
      
      set('create_book', {
        input: {
          title: form_data.get('title'),
          author: form_data.get('author'),
          category: form_data.get('category'),
          description: form_data.get('description'),
        },
      }).then(() => {
        instance.set_state({ 
          loading: false, 
          success: true,
          error: null,
        });
        
        // Clear the form
        event.target.reset();
        
        // Hide success message after 3 seconds
        setTimeout(() => {
          instance.set_state({ success: false });
        }, 3000);
      }).catch((error) => {
        instance.set_state({ 
          loading: false, 
          success: false,
          error: error.message || 'Failed to create book',
        });
      });
    },
  },
  css: `
    .add-book-form {
      max-width: 500px;
      margin: 0 auto;
      padding: 20px;
      border: 1px solid #ddd;
      border-radius: 8px;
    }

    .form-group {
      margin-bottom: 15px;
    }

    label {
      display: block;
      margin-bottom: 5px;
      font-weight: bold;
    }

    input, textarea, select {
      width: 100%;
      padding: 8px;
      border: 1px solid #ccc;
      border-radius: 4px;
      font-size: 14px;
    }

    textarea {
      height: 80px;
      resize: vertical;
    }

    button {
      background: #007bff;
      color: white;
      padding: 10px 20px;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 16px;
    }

    button:disabled {
      background: #ccc;
      cursor: not-allowed;
    }

    .success-message {
      background: #d4edda;
      color: #155724;
      padding: 10px;
      border-radius: 4px;
      margin-bottom: 15px;
    }

    .error-message {
      background: #f8d7da;
      color: #721c24;
      padding: 10px;
      border-radius: 4px;
      margin-bottom: 15px;
    }
  `,
  render: ({ state, when }) => {
    return `
      <div class="add-book-form">
        <h2>Add New Book</h2>
        
        ${when(state.success, `
          <div class="success-message">
            Book created successfully!
          </div>
        `)}
        
        ${when(state.error, `
          <div class="error-message">
            ${state.error}
          </div>
        `)}
        
        <form>
          <div class="form-group">
            <label for="title">Title</label>
            <input 
              type="text" 
              id="title" 
              name="title" 
              required 
              placeholder="Enter book title"
            />
          </div>
          
          <div class="form-group">
            <label for="author">Author</label>
            <input 
              type="text" 
              id="author" 
              name="author" 
              required 
              placeholder="Enter author name"
            />
          </div>
          
          <div class="form-group">
            <label for="category">Category</label>
            <select id="category" name="category" required>
              <option value="">Select a category</option>
              <option value="fiction">Fiction</option>
              <option value="non-fiction">Non-Fiction</option>
              <option value="science">Science</option>
              <option value="history">History</option>
              <option value="biography">Biography</option>
            </select>
          </div>
          
          <div class="form-group">
            <label for="description">Description (Optional)</label>
            <textarea 
              id="description" 
              name="description" 
              placeholder="Enter book description"
            ></textarea>
          </div>
          
          <button type="submit" ${state.loading ? 'disabled' : ''}>
            ${state.loading ? 'Creating...' : 'Create Book'}
          </button>
        </form>
      </div>
    `;
  },
});

export default AddBookForm;

Step 4: Add More Setters

Let's add setters for updating and deleting books. Update your api/books/setters.js file:

api/books/setters.js

import joystick from '@joystick.js/node';

const setters = {
  create_book: {
    input: {
      title: {
        type: 'string',
        required: true,
      },
      author: {
        type: 'string',
        required: true,
      },
      category: {
        type: 'string',
        required: true,
      },
      description: {
        type: 'string',
        required: false,
      },
    },
    set: (input = {}, context = {}) => {
      return process.databases.mongodb.collection('books').insertOne({
        _id: joystick.id(),
        title: input?.title,
        author: input?.author,
        category: input?.category,
        description: input?.description || '',
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      });
    },
  },
  
  update_book: {
    input: {
      book_id: {
        type: 'string',
        required: true,
      },
      title: {
        type: 'string',
        required: false,
      },
      author: {
        type: 'string',
        required: false,
      },
      category: {
        type: 'string',
        required: false,
      },
      description: {
        type: 'string',
        required: false,
      },
    },
    set: async (input = {}, context = {}) => {
      const update_data = {
        updated_at: new Date().toISOString(),
      };
      
      // Only update fields that were provided
      if (input.title) update_data.title = input.title;
      if (input.author) update_data.author = input.author;
      if (input.category) update_data.category = input.category;
      if (input.description !== undefined) update_data.description = input.description;
      
      const result = await process.databases.mongodb.collection('books').updateOne(
        { _id: input.book_id },
        { $set: update_data }
      );
      
      if (result.matchedCount === 0) {
        throw new Error('Book not found');
      }
      
      return result;
    },
  },
  
  delete_book: {
    input: {
      book_id: {
        type: 'string',
        required: true,
      },
    },
    set: async (input = {}, context = {}) => {
      const result = await process.databases.mongodb.collection('books').deleteOne({
        _id: input.book_id,
      });
      
      if (result.deletedCount === 0) {
        throw new Error('Book not found');
      }
      
      return result;
    },
  },
};

export default setters;

Step 5: Add Authorization to Your Setters

In a real application, you'll want to control who can create, update, or delete books. Let's add authorization:

api/books/setters.js

import joystick from '@joystick.js/node';

const setters = {
  create_book: {
    input: {
      title: {
        type: 'string',
        required: true,
      },
      author: {
        type: 'string',
        required: true,
      },
      category: {
        type: 'string',
        required: true,
      },
      description: {
        type: 'string',
        required: false,
      },
    },
    authorized: (input = {}, context = {}) => {
      // Only logged-in users can create books
      return !!context?.user;
    },
    set: (input = {}, context = {}) => {
      return process.databases.mongodb.collection('books').insertOne({
        _id: joystick.id(),
        title: input?.title,
        author: input?.author,
        category: input?.category,
        description: input?.description || '',
        created_by: context?.user?._id,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      });
    },
  },
  
  update_book: {
    input: {
      book_id: {
        type: 'string',
        required: true,
      },
      title: {
        type: 'string',
        required: false,
      },
      author: {
        type: 'string',
        required: false,
      },
      category: {
        type: 'string',
        required: false,
      },
      description: {
        type: 'string',
        required: false,
      },
    },
    authorized: async (input = {}, context = {}) => {
      if (!context?.user) {
        return {
          authorized: false,
          message: 'You must be logged in to update books',
        };
      }
      
      // Check if the user created this book
      const book = await process.databases.mongodb.collection('books').findOne({
        _id: input.book_id,
      });
      
      if (!book) {
        return {
          authorized: false,
          message: 'Book not found',
        };
      }
      
      if (book.created_by !== context.user._id) {
        return {
          authorized: false,
          message: 'You can only update books you created',
        };
      }
      
      return true;
    },
    set: async (input = {}, context = {}) => {
      const update_data = {
        updated_at: new Date().toISOString(),
      };
      
      if (input.title) update_data.title = input.title;
      if (input.author) update_data.author = input.author;
      if (input.category) update_data.category = input.category;
      if (input.description !== undefined) update_data.description = input.description;
      
      const result = await process.databases.mongodb.collection('books').updateOne(
        { _id: input.book_id },
        { $set: update_data }
      );
      
      return result;
    },
  },
  
  delete_book: {
    input: {
      book_id: {
        type: 'string',
        required: true,
      },
    },
    authorized: async (input = {}, context = {}) => {
      if (!context?.user) {
        return {
          authorized: false,
          message: 'You must be logged in to delete books',
        };
      }
      
      const book = await process.databases.mongodb.collection('books').findOne({
        _id: input.book_id,
      });
      
      if (!book) {
        return {
          authorized: false,
          message: 'Book not found',
        };
      }
      
      if (book.created_by !== context.user._id) {
        return {
          authorized: false,
          message: 'You can only delete books you created',
        };
      }
      
      return true;
    },
    set: async (input = {}, context = {}) => {
      const result = await process.databases.mongodb.collection('books').deleteOne({
        _id: input.book_id,
      });
      
      return result;
    },
  },
};

export default setters;

Step 6: Create a Book Management Component

Let's create a component that displays books and allows editing and deleting:

ui/components/book_manager/index.js

import joystick, { get, set } from '@joystick.js/ui';

const BookManager = joystick.component({
  state: {
    books: [],
    loading: true,
    editing_book: null,
    deleting_book: null,
  },
  lifecycle: {
    on_mount: (instance = {}) => {
      instance.methods.load_books();
    },
  },
  methods: {
    load_books: (instance = {}) => {
      instance.set_state({ loading: true });
      
      get('books').then((books) => {
        instance.set_state({ 
          books, 
          loading: false 
        });
      }).catch((error) => {
        console.error('Failed to load books:', error);
        instance.set_state({ loading: false });
      });
    },
    
    start_editing: (book = {}, instance = {}) => {
      instance.set_state({ editing_book: book });
    },
    
    cancel_editing: (instance = {}) => {
      instance.set_state({ editing_book: null });
    },
    
    save_book: (event = {}, instance = {}) => {
      event.preventDefault();
      
      const form_data = new FormData(event.target);
      const book_id = instance.state.editing_book._id;
      
      set('update_book', {
        input: {
          book_id,
          title: form_data.get('title'),
          author: form_data.get('author'),
          category: form_data.get('category'),
          description: form_data.get('description'),
        },
      }).then(() => {
        instance.set_state({ editing_book: null });
        instance.methods.load_books();
      }).catch((error) => {
        alert('Failed to update book: ' + error.message);
      });
    },
    
    delete_book: (book_id = '', instance = {}) => {
      if (!confirm('Are you sure you want to delete this book?')) {
        return;
      }
      
      instance.set_state({ deleting_book: book_id });
      
      set('delete_book', {
        input: { book_id },
      }).then(() => {
        instance.set_state({ deleting_book: null });
        instance.methods.load_books();
      }).catch((error) => {
        alert('Failed to delete book: ' + error.message);
        instance.set_state({ deleting_book: null });
      });
    },
  },
  events: {
    'click .edit-btn': (event = {}, instance = {}) => {
      const book_id = event.target.getAttribute('data-book-id');
      const book = instance.state.books.find(b => b._id === book_id);
      instance.methods.start_editing(book);
    },
    
    'click .delete-btn': (event = {}, instance = {}) => {
      const book_id = event.target.getAttribute('data-book-id');
      instance.methods.delete_book(book_id);
    },
    
    'click .cancel-btn': (event = {}, instance = {}) => {
      instance.methods.cancel_editing();
    },
    
    'submit .edit-form': (event = {}, instance = {}) => {
      instance.methods.save_book(event);
    },
  },
  css: `
    .book-manager {
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
    }

    .book-item {
      border: 1px solid #ddd;
      border-radius: 8px;
      padding: 15px;
      margin-bottom: 15px;
      background: #f9f9f9;
    }

    .book-header {
      display: flex;
      justify-content: space-between;
      align-items: flex-start;
      margin-bottom: 10px;
    }

    .book-title {
      font-size: 18px;
      font-weight: bold;
      margin: 0;
    }

    .book-author {
      color: #666;
      margin: 5px 0;
    }

    .book-category {
      background: #007bff;
      color: white;
      padding: 2px 8px;
      border-radius: 12px;
      font-size: 12px;
      display: inline-block;
    }

    .book-actions {
      display: flex;
      gap: 10px;
    }

    .edit-btn, .delete-btn, .cancel-btn {
      padding: 5px 10px;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 12px;
    }

    .edit-btn {
      background: #28a745;
      color: white;
    }

    .delete-btn {
      background: #dc3545;
      color: white;
    }

    .cancel-btn {
      background: #6c757d;
      color: white;
    }

    .edit-form {
      background: white;
      padding: 15px;
      border-radius: 8px;
      border: 2px solid #007bff;
    }

    .form-row {
      display: flex;
      gap: 15px;
      margin-bottom: 10px;
    }

    .form-group {
      flex: 1;
    }

    .form-group label {
      display: block;
      margin-bottom: 5px;
      font-weight: bold;
    }

    .form-group input, .form-group select, .form-group textarea {
      width: 100%;
      padding: 8px;
      border: 1px solid #ccc;
      border-radius: 4px;
    }

    .form-actions {
      display: flex;
      gap: 10px;
      justify-content: flex-end;
      margin-top: 15px;
    }

    .save-btn {
      background: #007bff;
      color: white;
      padding: 8px 16px;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }

    .loading {
      text-align: center;
      padding: 40px;
      color: #666;
    }

    .deleting {
      opacity: 0.5;
      pointer-events: none;
    }
  `,
  render: ({ state, when, each }) => {
    return `
      <div class="book-manager">
        <h2>Manage Books</h2>
        
        ${when(state.loading, `
          <div class="loading">Loading books...</div>
        `)}
        
        ${when(!state.loading && state.books.length === 0, `
          <p>No books found. Create your first book!</p>
        `)}
        
        ${each(state.books, (book) => `
          <div class="book-item ${state.deleting_book === book._id ? 'deleting' : ''}">
            ${when(state.editing_book && state.editing_book._id === book._id, `
              <form class="edit-form">
                <div class="form-row">
                  <div class="form-group">
                    <label>Title</label>
                    <input type="text" name="title" value="${book.title}" required />
                  </div>
                  <div class="form-group">
                    <label>Author</label>
                    <input type="text" name="author" value="${book.author}" required />
                  </div>
                </div>
                <div class="form-row">
                  <div class="form-group">
                    <label>Category</label>
                    <select name="category" required>
                      <option value="fiction" ${book.category === 'fiction' ? 'selected' : ''}>Fiction</option>
                      <option value="non-fiction" ${book.category === 'non-fiction' ? 'selected' : ''}>Non-Fiction</option>
                      <option value="science" ${book.category === 'science' ? 'selected' : ''}>Science</option>
                      <option value="history" ${book.category === 'history' ? 'selected' : ''}>History</option>
                      <option value="biography" ${book.category === 'biography' ? 'selected' : ''}>Biography</option>
                    </select>
                  </div>
                </div>
                <div class="form-group">
                  <label>Description</label>
                  <textarea name="description">${book.description || ''}</textarea>
                </div>
                <div class="form-actions">
                  <button type="button" class="cancel-btn">Cancel</button>
                  <button type="submit" class="save-btn">Save Changes</button>
                </div>
              </form>
            `, `
              <div class="book-header">
                <div>
                  <h3 class="book-title">${book.title}</h3>
                  <p class="book-author">by ${book.author}</p>
                  <span class="book-category">${book.category}</span>
                  ${when(book.description, `
                    <p style="margin-top: 10px; color: #666;">${book.description}</p>
                  `)}
                </div>
                <div class="book-actions">
                  <button class="edit-btn" data-book-id="${book._id}">Edit</button>
                  <button class="delete-btn" data-book-id="${book._id}">Delete</button>
                </div>
              </div>
            `)}
          </div>
        `)}
      </div>
    `;
  },
});

export default BookManager;

Step 7: Test Your Setters

Now let's create a page that uses both components to test our setters:

ui/pages/books/index.js

import joystick from '@joystick.js/ui';
import AddBookForm from '../../components/add_book_form/index.js';
import BookManager from '../../components/book_manager/index.js';

const BooksPage = joystick.component({
  css: `
    .books-page {
      max-width: 1200px;
      margin: 0 auto;
      padding: 20px;
    }

    .page-header {
      text-align: center;
      margin-bottom: 40px;
    }

    .sections {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 40px;
      align-items: flex-start;
    }

    @media (max-width: 768px) {
      .sections {
        grid-template-columns: 1fr;
      }
    }
  `,
  render: ({ component }) => {
    return `
      <div class="books-page">
        <div class="page-header">
          <h1>Book Management System</h1>
          <p>Create, update, and delete books using Joystick setters</p>
        </div>
        
        <div class="sections">
          ${component(AddBookForm)}
          ${component(BookManager)}
        </div>
      </div>
    `;
  },
});

export default BooksPage;

Key Concepts Learned

In this tutorial, you learned:

  1. Setter Structure: How to define setters with input validation and set functions
  2. Input Validation: Using Joystick's built-in validation system to ensure data quality
  3. Authorization: Implementing authorized() functions to control access to setters
  4. Database Operations: Creating, updating, and deleting data using MongoDB
  5. Error Handling: Properly handling and displaying errors from setter operations
  6. Form Handling: Using the set() method from @joystick.js/ui to call setters
  7. State Management: Managing loading states, success messages, and error states
  8. User Experience: Providing feedback during operations and confirmation for destructive actions

Next Steps

Now that you understand setters, you can:

  • Add more complex validation rules
  • Implement bulk operations (create/update/delete multiple items)
  • Add file upload capabilities to your setters
  • Implement soft deletes instead of hard deletes
  • Add audit logging to track changes
  • Create setters that interact with multiple collections/tables

Setters are powerful tools for building interactive applications. Combined with getters, they form the foundation of your app's data layer, enabling you to build rich, data-driven user experiences.